>100 Views
June 24, 25
スライド概要
Fresh 2 の安定版リリース時期が、2025年第3四半期後半(9月くらい)を目標にしていることがアナウンスされた。
Fresh 2 のAPIは ExpressやHonoに似たものになっているが「=」ではない。
これを踏まえ、いくつかのミドルウェアの実装パターンを共有する。
虎の穴ラボ株式会社は、主にとらのあな関連サービスのシステム開発を専門に担う、エンジニアの会社です。
Fresh 2 のミドルウェアを 触ってみる toranoana.deno #21 虎の穴ラボ オクタニ Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
自己紹介 奥谷 一陽 所属:虎の穴ラボ株式会社 仕事:Fantiaの開発 Ruby on Rails、 React 興味:Deno、TypeScript 最近購入:日本の家と町並み詳説絵巻 X:@okutann88 github:Octo8080X toranoana.deno 運営の1人 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
首 をながーくして 待 っていた Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
首 をながーくして 待 っていた Fresh 2 がついに 出 そうです Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
https://deno.com/blog/an-update-on-fresh Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
https://deno.com/blog/an-update-on-fresh 2025年第3四半期後半(9月くらい)を目標に進行中 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
Fresh 2 - コアAPIが簡素化、 Koa、Express、Hono のような APIに変更 - deno run -Ar jsr:@fresh/[email protected] Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved. で試せる。
初期状態 fresh 1だと
/// <reference no-default-lib="true" />
/// <reference lib="dom" />
/// <reference lib="dom.iterable" />
/// <reference lib="dom.asynciterable" />
/// <reference lib="deno.ns" />
import "$std/dotenv/load.ts";
Fresh の独自性を感じる API
Fresh1では、
- _middleware.tsへの記述
- プラグインで記述
の2つの方法があった。
import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";
import twindPlugin from "$fresh/plugins/twind.ts";
import twindConfig from "./twind.config.ts";
await start(manifest, {
plugins: [
twindPlugin(twindConfig),
],
});
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
初期状態 Fresh2 では
// main.ts
import { App, fsRoutes, staticFiles } from "fresh";
import { define, type State } from "./utils.ts";
どこかでみたことのある
.use や.get などを使う API
export const app = new App<State>();
app.use(staticFiles());
app.get("/api2/:name", (ctx) => {
const name = ctx.params.name;
return new Response(
`Hello, ${name.charAt(0).toUpperCase() + name.slice(1)}!`,
);
});
_middleware.tsへの記述できる、
プラグインの仕組みは残ってない
const exampleLoggerMiddleware = define.middleware((ctx) => {
console.log(`${ctx.req.method} ${ctx.req.url}`);
return ctx.next();
});
app.use(exampleLoggerMiddleware);
await fsRoutes(app, {
loadIsland: (path) => import(`./islands/${path}`),
loadRoute: (path) => import(`./routes/${path}`),
});
if (import.meta.main) {
await app.listen();
}
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
ミドルウェアの型
// main.ts
const exampleLoggerMiddleware = define.middleware((ctx) => {
console.log(`${ctx.req.method} ${ctx.req.url}`);
return ctx.next();
});
app.use(exampleLoggerMiddleware);
define(=createDefine関数の結果)
により
各ミドルウェア間で共有する情報が
一括で記述される
(使用例は後述します)
// fresh本体リポジトリ src/define.ts 抜粋
export interface Define<State> {
// handlers pages などもあるが、ここでは省略
middleware<M extends Middleware<State>>(middleware: M): typeof middleware;
}
// fresh本体リポジトリ src\middlewares\mod.ts 抜粋
export type Middleware<State> = MiddlewareFn<State> | MiddlewareFn<State>[];
export type MiddlewareFn<State> = (
ctx: FreshContext<State>,
) => Response | Promise<Response>;
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
Fresh 2 でのミドルウェア Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
Fresh 2 でのミドルウェア 実装例 - 状態、パラメータを持たないミドルウェア - 状態/パラメータを持つミドルウェア - メソッドを公開するミドルウェア Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
状態、パラメータを持たないミドルウェア
/// middlewares\simple_logger.ts
import { define } from '../utils.ts';
export const simpleLoggerMiddleware = define.middleware((ctx) => {
console.log(`${new Date().toISOString()} ${ctx.req.method}: ${ctx.req.url}`);
return ctx.next();
});
/// main.ts
import { App, fsRoutes, staticFiles } from "fresh";
import { type State } from "./utils.ts";
import { simpleLoggerMiddleware } from
"./middlewares/simple_logger.ts";
export const app = new App<State>();
app.use(staticFiles());
app.use(simpleLoggerMiddleware);
ただのロガーのようなモノ
が当たるケース
await fsRoutes(app, {
loadIsland: (path) => import(`./islands/${path}`),
loadRoute: (path) => import(`./routes/${path}`),
});
if (import.meta.main) {
await app.listen();
}
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
状態/パラメータを持つミドルウェア
/// middlewares/simple_late_limit.ts
import { define } from "../utils.ts";
export function simpleLateLimit(limitPerMinute: number) {
const requests = new Map<string, number>();
const getIpWithTime = (ip: string) => `${ip}-${Math.floor(Date.now() / 60000)}`
return define.middleware(async (ctx) => {
const hostname = (ctx.info.remoteAddr as { hostname: string }).hostname;
const ip = ctx.req.headers.get("X-Forwarded-For") || ctx.req.headers.get("X-Real-IP") || hostname;
/// 省略
const res = await ctx.next();
res.headers.set("X-RateLimit-Remaining", String(Math.max(0, limitPerMinute - (requests.get(key) ||
0))));
return res;
});
}
/// main.ts
app.use(simpleLateLimit(100));
クラス、クロージャ ―で構成し、ミドルウェアを返す関数を構築する
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
メソッドを公開するミドルウェア
/// middlewares\logger_with_id.ts
import { define } from "../utils.ts";
export interface LoggerStatus {
logger: {
log: (message: string) => void;
}
}
export const loggerWithIdMiddleware = define.middleware((ctx) => {
const logId = crypto.randomUUID();
const logMessage = (message: any) => `${new Date().toISOString()} [${logId}] ${message.toString()}`;
ctx.state.logger = {
log: (message: string) => console.log(logMessage(message)),
};
return ctx.next();
});
メソッドを公開に当たり、 ctx.status.logger.log があるという型も提供する
この時点では、型エラーを起こす!
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
メソッドを公開するミドルウェア
/// utils.ts
import { createDefine } from "fresh";
import { LoggerStatus } from './middlewares/logger_with_id.ts';
// deno-lint-ignore no-empty-interface
export interface BaseState {}
export interface State extends BaseState, LoggerStatus {}
<= 型を合成
export const define = createDefine<State>();
createDefine の型引数にミドルウェアとして必要な型を追加する
この記述がされて始めて、型エラーが解消される。
createDefineは、middleware やpageなどのメソッドを持ち、
定義に合わせた型情報を持たせた実装をサポートする
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
メソッドを公開するミドルウェア
/// utils.ts
import { useSignal } from "@preact/signals";
import { define } from "../utils.ts";
import Counter from "../islands/Counter.tsx";
export default define.page(function Home(ctx) { /// ctx は、FreshContext<State>であると定義されており、任意の記述は不要
const count = useSignal(3);
ctx.state.logger.log("Rendering Home page");
return (
<div class="px-4 py-8 mx-auto fresh-gradient">
{/* 省略 */}
</div>
);
});
$ deno task dev
# 省略
2025-06-08T04:28:37.439Z [768fd1cf-580d-4380-b8f7-b4a99af755b8] Rendering Home page
define.pageの定義に基づき、
ctxはstate.logger.logがあるので型エラーなく使用できる
Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
まとめ/これから - Fresh 2 は、Fresh 1系とAPIが異なる。 - - - Fresh 2 はAPIの構造が、 Express、Koa、Honoに似ている - しかし、そのものではなくそのまま転用できる訳ではない。 - .useは/* 相当の割り当てしかできないなど違いもある。 - 近しい構造になったことで、移植もしやすくなったのでは? createDefine 関数を使いこなせると、全体に楽ができる。 - - ミドルウェアには互換性なし。 逆に使わないと型周りでの苦労が見込まれる。 ミドルウェアの構造が把握できたので、 サードパーティミドルウエアとしているモノも対応を進める。 Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.
ありがとうございました Copyright (C) 2025 Toranoana Lab Inc. All Rights Reserved.