NestJSのMiddlewareは、ルートハンドラ(Controller)に到達する前にリクエストを処理する仕組みです。ロギング、認証チェック、リクエストの変換など、複数のエンドポイントに共通する処理を一元化できます。本記事では、NestMiddlewareインターフェースを使用したクラスベースのMiddleware実装から、forRoutes()による適用範囲の柔軟な設定、そして実践的なロギングMiddlewareの構築までを詳しく解説します。
実行環境と前提条件#
本記事の内容を実践するにあたり、以下の環境を前提としています。
| 項目 |
バージョン・要件 |
| Node.js |
20以上 |
| npm |
10以上 |
| NestJS |
11.x |
| OS |
Windows / macOS / Linux |
| エディタ |
VS Code(推奨) |
事前に以下の準備を完了してください。
- NestJS CLIのインストール済み
- NestJSプロジェクトの作成済み
プロジェクトの作成方法はNestJS入門記事を参照してください。Module・Controller・Providerの基本はアーキテクチャ解説記事で確認できます。
Middlewareとは#
Middlewareは、ルートハンドラが実行される前に呼び出される関数です。Expressのmiddlewareと同様に、リクエストオブジェクト(req)、レスポンスオブジェクト(res)、および次のmiddleware関数(next)にアクセスできます。
flowchart LR
A[Client Request] --> B[Middleware 1]
B --> C[Middleware 2]
C --> D[Guard]
D --> E[Interceptor]
E --> F[Pipe]
F --> G[Controller]
G --> H[Response]
style B fill:#4a9eff,color:#fff
style C fill:#4a9eff,color:#fffMiddlewareで実行できる処理#
Middlewareでは以下の処理を実行できます。
- 任意のコードの実行
- リクエストオブジェクトとレスポンスオブジェクトの変更
- リクエスト・レスポンスサイクルの終了
- スタック内の次のmiddleware関数の呼び出し
重要な点として、現在のmiddlewareがリクエスト・レスポンスサイクルを終了しない場合は、必ずnext()を呼び出して次のmiddlewareに制御を渡す必要があります。そうしないと、リクエストがハングした状態になります。
NestJSリクエストライフサイクルにおけるMiddlewareの位置#
NestJSのリクエスト処理パイプラインでは、Middlewareは最初に実行されます。
| 実行順序 |
コンポーネント |
主な役割 |
| 1 |
Middleware |
リクエスト前処理、ロギング、変換 |
| 2 |
Guard |
認可チェック |
| 3 |
Interceptor(前処理) |
リクエスト変換、キャッシュ |
| 4 |
Pipe |
バリデーション、データ変換 |
| 5 |
Controller |
ビジネスロジック実行 |
| 6 |
Interceptor(後処理) |
レスポンス変換 |
クラスベースのMiddleware実装#
NestJSでは、@Injectable()デコレータを持つクラスでNestMiddlewareインターフェースを実装することで、Middlewareを作成します。
基本的なLoggerMiddlewareの実装#
まず、シンプルなロギングMiddlewareを作成します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/common/middleware/logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
const { method, originalUrl } = req;
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${method} ${originalUrl}`);
next();
}
}
|
NestMiddlewareインターフェースはuseメソッドの実装を要求します。このメソッドは、Express(またはFastify)のmiddleware関数と同じシグネチャを持ちます。
Middlewareの適用#
Middlewareを適用するには、モジュールクラスでNestModuleインターフェースを実装し、configure()メソッドを定義します。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { UsersModule } from './users/users.module';
@Module({
imports: [UsersModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('users');
}
}
|
この設定により、/usersで始まるすべてのルートにLoggerMiddlewareが適用されます。
forRoutes()による適用範囲の設定#
MiddlewareConsumerのforRoutes()メソッドは、Middlewareの適用範囲を柔軟に設定できます。
パスとHTTPメソッドによる制限#
特定のHTTPメソッドにのみMiddlewareを適用できます。
1
2
3
4
5
6
7
8
9
10
11
|
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'users', method: RequestMethod.GET });
}
}
|
Controllerクラスによる指定#
ルートパスの代わりにControllerクラスを指定することで、そのController内のすべてのルートにMiddlewareを適用できます。
1
2
3
4
5
6
7
8
9
10
11
12
|
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { UsersController } from './users/users.controller';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(UsersController);
}
}
|
ワイルドカードによるパターンマッチング#
ルートパスにはワイルドカードを使用できます。NestJS 11以降では、名前付きワイルドカード(*splat)を使用します。
1
2
3
4
5
|
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'users/*splat', method: RequestMethod.ALL });
}
|
ワイルドカードをオプショナルにする場合は、波括弧で囲みます。
1
2
|
// 'users/' と 'users/123' の両方にマッチ
forRoutes({ path: 'users/{*splat}', method: RequestMethod.ALL });
|
複数のControllerへの適用#
カンマ区切りで複数のControllerを指定できます。
1
2
3
4
5
|
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(UsersController, ProductsController, OrdersController);
}
|
exclude()による除外設定#
特定のルートをMiddleware適用対象から除外できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { UsersController } from './users/users.controller';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'users/health', method: RequestMethod.GET },
{ path: 'users/metrics', method: RequestMethod.GET },
)
.forRoutes(UsersController);
}
}
|
この設定では、UsersControllerのすべてのルートにLoggerMiddlewareが適用されますが、GET /users/healthとGET /users/metricsは除外されます。
関数型Middleware#
依存関係を必要としないシンプルなMiddlewareは、関数として定義することもできます。
1
2
3
4
5
6
7
|
// src/common/middleware/simple-logger.middleware.ts
import { Request, Response, NextFunction } from 'express';
export function simpleLogger(req: Request, res: Response, next: NextFunction) {
console.log(`Request... ${req.method} ${req.originalUrl}`);
next();
}
|
関数型Middlewareの適用方法はクラスベースと同じです。
1
2
3
4
5
6
7
|
import { simpleLogger } from './common/middleware/simple-logger.middleware';
configure(consumer: MiddlewareConsumer) {
consumer
.apply(simpleLogger)
.forRoutes('*');
}
|
依存性注入が不要な場合は、関数型Middlewareの使用が推奨されます。
複数のMiddlewareの適用#
apply()メソッドには複数のMiddlewareをカンマ区切りで渡すことができます。Middlewareは指定した順序で実行されます。
1
2
3
4
5
6
7
8
9
|
import { corsMiddleware } from './common/middleware/cors.middleware';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { AuthMiddleware } from './common/middleware/auth.middleware';
configure(consumer: MiddlewareConsumer) {
consumer
.apply(corsMiddleware, LoggerMiddleware, AuthMiddleware)
.forRoutes('*');
}
|
グローバルMiddlewareの登録#
すべてのルートにMiddlewareを適用する場合、main.tsでapp.use()を使用できます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { simpleLogger } from './common/middleware/simple-logger.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// グローバルMiddlewareの登録
app.use(simpleLogger);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
|
グローバルMiddlewareで依存性注入を使用したい場合は、app.use()ではなくforRoutes('*')を使用してください。
1
2
3
4
5
|
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('*');
}
|
依存性注入を活用したMiddleware#
クラスベースのMiddlewareは、NestJSの依存性注入システムを完全にサポートしています。
ConfigServiceを利用したMiddleware#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// src/common/middleware/configurable-logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class ConfigurableLoggerMiddleware implements NestMiddleware {
private readonly logLevel: string;
constructor(private readonly configService: ConfigService) {
this.logLevel = this.configService.get<string>('LOG_LEVEL', 'info');
}
use(req: Request, res: Response, next: NextFunction) {
if (this.shouldLog()) {
const { method, originalUrl, ip } = req;
const userAgent = req.get('user-agent') || '';
const timestamp = new Date().toISOString();
console.log(
`[${timestamp}] ${method} ${originalUrl} - ${ip} - ${userAgent}`
);
}
next();
}
private shouldLog(): boolean {
return this.logLevel === 'debug' || this.logLevel === 'info';
}
}
|
カスタムLoggerサービスを注入したMiddleware#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
// src/common/services/app-logger.service.ts
import { Injectable, LoggerService } from '@nestjs/common';
@Injectable()
export class AppLoggerService implements LoggerService {
log(message: string) {
console.log(`[LOG] ${new Date().toISOString()} - ${message}`);
}
error(message: string, trace?: string) {
console.error(`[ERROR] ${new Date().toISOString()} - ${message}`);
if (trace) {
console.error(trace);
}
}
warn(message: string) {
console.warn(`[WARN] ${new Date().toISOString()} - ${message}`);
}
debug(message: string) {
console.debug(`[DEBUG] ${new Date().toISOString()} - ${message}`);
}
verbose(message: string) {
console.log(`[VERBOSE] ${new Date().toISOString()} - ${message}`);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
// src/common/middleware/advanced-logger.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { AppLoggerService } from '../services/app-logger.service';
@Injectable()
export class AdvancedLoggerMiddleware implements NestMiddleware {
constructor(private readonly logger: AppLoggerService) {}
use(req: Request, res: Response, next: NextFunction) {
const { method, originalUrl, ip } = req;
const startTime = Date.now();
// レスポンス完了時のログ出力
res.on('finish', () => {
const duration = Date.now() - startTime;
const { statusCode } = res;
this.logger.log(
`${method} ${originalUrl} ${statusCode} - ${duration}ms - ${ip}`
);
});
next();
}
}
|
実践的なMiddleware実装例#
リクエストタイムログMiddleware#
リクエストの処理時間を計測し、ログに出力するMiddlewareです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
// src/common/middleware/request-time.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class RequestTimeMiddleware implements NestMiddleware {
private readonly logger = new Logger('HTTP');
use(req: Request, res: Response, next: NextFunction) {
const { method, originalUrl } = req;
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
const { statusCode } = res;
const contentLength = res.get('content-length') || 0;
const logMessage = `${method} ${originalUrl} ${statusCode} ${contentLength} - ${duration}ms`;
if (statusCode >= 500) {
this.logger.error(logMessage);
} else if (statusCode >= 400) {
this.logger.warn(logMessage);
} else {
this.logger.log(logMessage);
}
});
next();
}
}
|
リクエストIDMiddleware#
各リクエストに一意のIDを付与し、ログのトレーサビリティを向上させます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
// src/common/middleware/request-id.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
// Requestにカスタムプロパティを追加する型定義
declare global {
namespace Express {
interface Request {
requestId?: string;
}
}
}
@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// ヘッダーから既存のリクエストIDを取得、なければ生成
const requestId = req.get('X-Request-ID') || randomUUID();
// リクエストオブジェクトにIDを付与
req.requestId = requestId;
// レスポンスヘッダーにもIDを設定
res.set('X-Request-ID', requestId);
next();
}
}
|
CORSMiddleware#
クロスオリジンリクエストを許可するMiddlewareの実装例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
// src/common/middleware/cors.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class CorsMiddleware implements NestMiddleware {
private readonly allowedOrigins: string[];
constructor(private readonly configService: ConfigService) {
const origins = this.configService.get<string>('CORS_ORIGINS', '*');
this.allowedOrigins = origins.split(',').map((o) => o.trim());
}
use(req: Request, res: Response, next: NextFunction) {
const origin = req.get('Origin');
if (this.isOriginAllowed(origin)) {
res.set('Access-Control-Allow-Origin', origin || '*');
}
res.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, PATCH, OPTIONS');
res.set('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Request-ID');
res.set('Access-Control-Allow-Credentials', 'true');
res.set('Access-Control-Max-Age', '86400');
// プリフライトリクエストの処理
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}
next();
}
private isOriginAllowed(origin: string | undefined): boolean {
if (!origin) return true;
if (this.allowedOrigins.includes('*')) return true;
return this.allowedOrigins.includes(origin);
}
}
|
実際のプロダクション環境では、NestJSの組み込みCORS機能(app.enableCors())を使用することが推奨されます。上記はMiddlewareの実装例として参考にしてください。
セキュリティヘッダーMiddleware#
セキュリティ関連のHTTPヘッダーを設定するMiddlewareです。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// src/common/middleware/security-headers.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
@Injectable()
export class SecurityHeadersMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
// XSSフィルターを有効化
res.set('X-XSS-Protection', '1; mode=block');
// MIMEタイプのスニッフィングを防止
res.set('X-Content-Type-Options', 'nosniff');
// クリックジャッキング対策
res.set('X-Frame-Options', 'DENY');
// Referrer情報の制御
res.set('Referrer-Policy', 'strict-origin-when-cross-origin');
// HTTPSの強制(本番環境のみ)
if (process.env.NODE_ENV === 'production') {
res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
next();
}
}
|
プロダクション環境では、helmetライブラリの使用も検討してください。
Middlewareモジュールの構成例#
複数のMiddlewareを整理するためのモジュール構成例です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// src/common/common.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { RequestIdMiddleware } from './middleware/request-id.middleware';
import { RequestTimeMiddleware } from './middleware/request-time.middleware';
import { SecurityHeadersMiddleware } from './middleware/security-headers.middleware';
import { AppLoggerService } from './services/app-logger.service';
@Module({
imports: [ConfigModule],
providers: [AppLoggerService],
exports: [AppLoggerService],
})
export class CommonModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
RequestIdMiddleware,
SecurityHeadersMiddleware,
RequestTimeMiddleware,
)
.forRoutes('*');
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { CommonModule } from './common/common.module';
import { UsersModule } from './users/users.module';
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
CommonModule,
UsersModule,
],
})
export class AppModule {}
|
期待される動作の確認#
Middlewareが正しく動作しているかを確認するため、アプリケーションを起動してリクエストを送信します。
1
2
|
# アプリケーションの起動
npm run start:dev
|
別のターミナルでリクエストを送信します。
1
2
|
# GETリクエストの送信
curl -i http://localhost:3000/users
|
期待される出力例(コンソール):
[2026-01-06T08:00:00.000Z] GET /users
[LOG] 2026-01-06T08:00:00.050Z - GET /users 200 - 50ms - ::1
レスポンスヘッダーの確認:
HTTP/1.1 200 OK
X-Request-ID: 550e8400-e29b-41d4-a716-446655440000
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
...
MiddlewareとInterceptor/Guardの使い分け#
NestJSでは、リクエスト処理に関与する複数のコンポーネントがあります。適切なコンポーネントの選択が重要です。
| コンポーネント |
主な用途 |
実行コンテキスト |
DI |
| Middleware |
ロギング、ヘッダー操作、リクエスト変換 |
Express/Fastify |
可 |
| Guard |
認可チェック、アクセス制御 |
NestJS ExecutionContext |
可 |
| Interceptor |
レスポンス変換、キャッシュ、実行時間計測 |
NestJS ExecutionContext |
可 |
選択の指針#
- Middleware: HTTPレイヤーでの汎用的な処理(ロギング、CORSなど)
- Guard: ルートへのアクセス可否の判定(認証・認可)
- Interceptor: リクエスト/レスポンスの変換、横断的関心事
MiddlewareはExecutionContextにアクセスできないため、どのController/Handlerが実行されるかを知ることができません。ルートハンドラの情報が必要な場合は、GuardまたはInterceptorを使用してください。
まとめ#
本記事では、NestJSのMiddlewareについて以下の内容を解説しました。
- Middlewareの基本概念とリクエストライフサイクルにおける位置づけ
NestMiddlewareインターフェースを使用したクラスベースのMiddleware実装
forRoutes()とexclude()による柔軟な適用範囲の設定
- 関数型Middlewareの実装と使い分け
- 依存性注入を活用した実践的なMiddleware(ロギング、リクエストID、セキュリティヘッダー)
Middlewareを適切に活用することで、Controllerのコードをシンプルに保ちながら、共通処理を効率的に実装できます。次回の記事では、Pipeによるバリデーションとデータ変換について解説します。
参考リンク#