メインコンテンツまでスキップ

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が優先されます。

CorsMiddlewareSecureHeadersMiddlewareは、全ての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になります)。この例では AuthMiddlewarenext(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は次の順序で実行されます:

  1. グローバルmiddleware(配列の順序どおり)
  2. controller middleware(デコレータの順序どおり)
  3. メソッドmiddleware(デコレータの順序どおり)
  4. ルートハンドラ
  5. ハンドラ後の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`);
  }
}