Middleware
Middlewareクラスはルートハンドラの前に実行され、リクエスト、レスポンス、またはcontextを変更できます。
Class Middleware
最もシンプルなmiddlewareの形は、use()メソッドを持つクラスです。HTTP primitiveへアクセスするにはrequest()とresponse()を使います:
import { Middleware, request, type Next } from '@zeltjs/core';
@Middleware
export class LoggingMiddleware {
async use(next: Next, req = request()): Promise<Response | undefined> {
const start = Date.now();
await next();
const duration = Date.now() - start;
console.log(`[${req.method()}] ${req.path()} ${duration}ms`);
return undefined;
}
}
Middleware Levels
Zeltは3つのレベルでmiddlewareをサポートしており、global → controller → methodの順に実行されます。
Global Middleware
createApp()を通じて全てのルートへ適用します:
export const app = createApp([http({
controllers: [UserController],
middlewares: [LoggingMiddleware],
})]);
Controller Middleware
@UseMiddlewareでcontroller内の全メソッドへ適用します:
@UseMiddleware(AuthMiddleware)
@Controller('/admin')
export class AdminController {
@Get('/dashboard')
dashboard() {
return { stats: [] };
}
}
Method Middleware
特定のメソッドへ適用します:
@Controller('/posts')
export class PostController {
@Get('/')
findAll() {
return { posts: [] };
}
@UseMiddleware(AdminOnlyMiddleware)
@Delete('/:id')
remove(req = request()) {
const id = req.pathParam('id');
return { deleted: id };
}
}
Skipping Middleware
@SkipMiddlewareを使うと、特定のmiddlewareをメソッドから除外できます:
@Controller('/api')
export class ApiController {
@Get('/protected')
protected() {
return { secret: 'data' };
}
@SkipMiddleware(AuthMiddleware)
@Get('/health')
health() {
return { status: 'ok' };
}
}
@SkipMiddlewareをcontrollerクラスに適用すると、そのcontroller内の全てのルートからmiddlewareを除外できます:
@SkipMiddleware(AuthMiddleware)
@Controller('/public')
export class PublicController {
@Get('/health')
health() {
return { status: 'ok' };
}
@Get('/version')
version() {
return { version: '1.0.0' };
}
}
クラスレベルとメソッドレベルのskip宣言は組み合わされます。controllerがAuthMiddlewareをskipし、メソッドがLoggingMiddlewareをskipしている場合、そのメソッドは両方をskipします。
より具体的なmiddlewareのアタッチは、クラスレベルのskipよりも優先されます。controllerに@SkipMiddleware(AuthMiddleware)が付いていても、あるメソッドに@UseMiddleware(AuthMiddleware)が付いていれば、そのメソッドではAuthMiddlewareが実行されます。同じメソッドに@UseMiddleware(AuthMiddleware)と@SkipMiddleware(AuthMiddleware)の両方が付いている場合は、メソッドレベルのskipが優先されます。
CorsMiddlewareとSecureHeadersMiddlewareは、全てのHTTPアプリで自動的に登録されます。デフォルト設定、設定オプション、skipの例、CORSプリフライトの挙動についてはHTTP Securityを参照してください。
Middleware Values
middlewareは、後続のコードに型付きの値を提供できます。値の提供には Next<T> 型と next(value) を、読み取りには middlewareValue(M) を使います。文字列キーやmodule augmentationを管理する必要はありません。
Providing a Value
値を提供するmiddlewareは、それを Next<T> に宣言し、next(value) に渡します。
@Middleware
export class AuthMiddleware {
async use(next: Next<{ id: number; name: string }>, req = request()): Promise<Response | undefined> {
const token = req.header('Authorization');
const user = token ? await verifyToken(token) : null;
if (!user) return Response.json({ error: 'Unauthorized' }, { status: 401 });
await next(user);
return undefined;
}
}
Next<T> は引数を必須とするため、値を渡さずに next() を呼び出すと型エラーになります。何も提供しないmiddlewareは、これまでの例のようにそのまま Next 型を使い続けます。
Reading a Value
handler、そして他のmiddlewareも、提供された値をパラメータのデフォルト値として渡す middlewareValue(M) で読み取ります。
@UseMiddleware(AuthMiddleware)
@Controller('/profile')
export class ProfileController {
@Get('/')
getProfile(user = middlewareValue(AuthMiddleware)) {
return { id: user.id, name: user.name };
}
}
型は M 自身の Next<T> 宣言をそのまま反映します(例えば Next<string | undefined> を宣言するmiddlewareであれば、読み取れる値もnullableになります)。この例では AuthMiddleware が next(user) を呼ぶ前に401レスポンスで短絡するため、handlerが実行される時点で値の存在は保証されています。middlewareが他のmiddlewareの値を読み取る場合も、自身のuse()メソッドのパラメータのデフォルト値として同じ方法で読み取ります。
Reading Requires the Middleware
middlewareValue(M) の呼び出しは、次の2つの場合に即座にそのmiddleware名を含むエラーをスローします — ルートにそのmiddlewareが適用されていない場合、そして適用されていてもnext(value)が一度も呼ばれておらず値が記録されていない場合です。黙ってundefinedが返ることはありません。middlewareをルートに適用する方法自体は従来どおりで、controllerやmethodへの@UseMiddleware、またはmoduleのmiddlewaresを使います。
Dependency Injection
依存性注入が必要なmiddlewareでは、@Middlewareを使います:
import { Config, Env, Middleware, inject, request } from '@zeltjs/core';
import type { Next } from '@zeltjs/core';
@Config
class AuthConfig {
static readonly Token = AuthConfig;
constructor(private env = inject(Env)) {}
get secret() {
return this.env.getString('AUTH_SECRET');
}
}
@Middleware
export class AuthMiddleware {
constructor(private config = inject(AuthConfig)) {}
async use(next: Next, req = request()): Promise<Response | undefined> {
const secret = this.config.secret;
// ... 認証ロジック
await next();
return undefined;
}
}
class middlewareは、function middlewareと同じ方法で使えます:
@UseMiddleware(AuthMiddleware)
@Controller('/admin')
export class AdminController {
@Get('/') index() { return { ok: true }; }
}
Middleware with Options
設定が必要なmiddlewareはMiddlewareWithOptions<TOptions>を継承し、オプションをmiddlewareOptions()でパラメータデフォルト値として読み取ります — request()やmiddlewareValue()と同じパターンです:
interface RateLimitOptions {
limit: number;
windowSec: number;
}
@Middleware
export class RateLimitMiddleware extends MiddlewareWithOptions<RateLimitOptions> {
async use(next: Next, opts = middlewareOptions(RateLimitMiddleware)) {
const { limit, windowSec } = opts;
// ... レート制限ロジック
await next();
return undefined;
}
}
オプションは、middlewareを適用する場所で.with()を使って渡します:
@Controller('/api')
export class ApiController {
@UseMiddleware(RateLimitMiddleware.with({ limit: 10, windowSec: 60 }))
@Post('/submit')
submit() {
return { submitted: true };
}
}
オプションを取るmiddlewareの登録は常に.with()を通して行います。素のクラスをそのまま登録すると型エラーになります — 実行に使うオプションが存在しないためです。
オプションを持たないmiddlewareは何も継承せず、これまでの例のとおり素のクラスをそのまま登録します。
オプション付きmiddlewareの値を読む
オプションを取るmiddlewareが提供する値を読むには、.with()の戻り値をconstに入れ、middlewareを適用する場所とmiddlewareValue()の両方で同じconstを使います:
// user-auth.middleware.ts
export const adminAuth = UserAuthMiddleware.with({ role: 'admin' });
// admin.controller.ts
@UseMiddleware(adminAuth)
@Controller('/admin')
export class AdminController {
@Get('/me')
me(admin = middlewareValue(adminAuth)) {
return { id: admin.id, role: admin.role };
}
}
.with()は呼び出しごとに、それぞれ別のmiddlewareとして扱われます。ルートの登録に使った.with()とmiddlewareValue()に渡した.with()が別の呼び出しだと、たとえオプションが同一でも両者は一致しません — middlewareValue()からはルートに適用されていないmiddlewareに見え、例外になります。必ず1つのconstを共有してください。
同じmiddlewareクラスを異なるオプションで複数回適用することもできます。それぞれの適用は独立して実行され、各constは自分の値を読み取ります。
Request Flow
Middlewareはawait next()の前後どちらにロジックを置くかによって、ルートハンドラの前後両方を処理できます。
Execution Order
Middlewareは次の順序で実行されます:
- グローバルmiddleware(配列の順序どおり)
- controller middleware(デコレータの順序どおり)
- メソッドmiddleware(デコレータの順序どおり)
- ルートハンドラ
- ハンドラ後のmiddleware(
next()後は逆順)
@Middleware
class GlobalMiddleware {
async use(next: Next) {
console.log('1. global before');
await next();
console.log('6. global after');
}
}
@Middleware
class ControllerMiddleware {
async use(next: Next) {
console.log('2. controller before');
await next();
console.log('5. controller after');
}
}
@Middleware
class MethodMiddleware {
async use(next: Next) {
console.log('3. method before');
await next();
console.log('4. method after');
}
}
Common Patterns
Middlewareはクラスとして記述します。フレームワークのprimitiveにはrequest()、response()、middlewareValue()を使います。
Restrict Access
serviceを注入する必要がある場合はclass middlewareを使います:
@Middleware
export class RequireAdmin {
constructor(private authService = inject(AuthService)) {}
async use(next: Next): Promise<Response | undefined> {
const user = currentUser();
if (!this.authService.isAdmin(user)) {
return Response.json({ error: 'Forbidden' }, { status: 403 });
}
await next();
return undefined;
}
}
Add Response Headers
レスポンスヘッダーにはresponse()を使います:
@Middleware
class PoweredByMiddleware {
async use(next: Next, res = response()) {
res.header('X-Powered-By', 'zelt');
await next();
}
}
同じヘッダーで複数の値を保持したい場合は{ type: 'append' }を使います:
@Middleware
class CacheTagMiddleware {
async use(next: Next, res = response()) {
res.header('Cache-Tag', 'api');
res.header('Cache-Tag', 'users', { type: 'append' });
await next();
}
}
Measure Response Time
@Middleware
class TimingMiddleware {
async use(next: Next, res = response()) {
const start = Date.now();
await next();
res.header('X-Response-Time', `${Date.now() - start}ms`);
}
}