TypeScriptでミドルウェアを設計する ExpressとNestJSの応用例で型安全に整理する
TypeScript でミドルウェアを設計する際、Express と NestJS のどちらを選ぶべきか、型安全性をどう担保するか、実務では常に判断を迫られます。本記事では、ミドルウェア設計における Express と NestJS の違いを比較し、型安全な実装パターンと実務での判断基準を整理します。
リクエスト拡張や共通コンテキストの扱い、インターフェース設計、ユニオン型の活用など、拡張しやすく壊れにくい設計を、実際の検証結果と失敗経験を交えて解説します。
Express と NestJS のミドルウェア設計比較
| # | 観点 | Express | NestJS | 実務での選択基準 |
|---|---|---|---|---|
| 1 | 設計思想 | 手続き型・柔軟性重視 | 宣言型・構造化重視 | 小規模は Express、大規模は NestJS |
| 2 | 型安全性 | 手動で型定義が必要 | デコレーターで型推論が効く | 型安全重視なら NestJS |
| 3 | Request拡張 | インターフェース拡張が必要 | デコレーターで自動注入 | 拡張頻度が高いなら NestJS |
| 4 | 学習コスト | 低い | 中〜高い | チームの習熟度に依存 |
| 5 | 保守性 | 設計次第で変動大 | 高い(DI・デコレーター) | 長期運用なら NestJS |
この表は即答用です。詳細な理由と実装例は後段で解説します。
検証環境
- OS: macOS 15.2
- Node.js: 22.13.1
- TypeScript: 5.7.2
- 主要パッケージ:
- express: 4.21.2
- @nestjs/core: 10.4.15
- @nestjs/common: 10.4.15
- 検証日: 2026年1月7日
TypeScript ミドルウェア設計における背景と実務での課題
この章でわかること:
- なぜ TypeScript でミドルウェアを設計するのか
- Express と NestJS の設計思想の違い
- 実務で直面する型安全性の課題
TypeScript によるミドルウェア設計が求められる理由
モダンな Web アプリケーション開発において、ミドルウェアは認証・ログ・バリデーションといった横断的関心事を担います。JavaScript だけで実装すると、リクエストオブジェクトへの動的なプロパティ追加により、実行時エラーが頻発します。
TypeScript を導入することで、型安全性が向上し、コンパイル時にエラーを検出できるようになります。しかし、Express と NestJS では型定義のアプローチが大きく異なり、実務では どちらを選ぶべきか という判断が求められます。
実務で直面した型安全性の課題
実際に業務で Express プロジェクトを TypeScript 化した際、以下の問題に遭遇しました。
req.userに型定義がなく、実行時にundefinedエラーが発生- ミドルウェアチェーンでの型推論が効かず、手動で型アサーションが必要
- インターフェース拡張が複数ファイルに散らばり、保守困難
この経験から、型安全性を担保する設計パターンと Express / NestJS の使い分け基準の重要性を痛感しました。
つまずきポイント
- Express では
Request型の拡張をdeclare globalで行う必要があり、初学者には難易度が高い - NestJS はデコレーター記法が独特で、学習コストが高い
Express と NestJS の設計思想と型安全性の違い
この章でわかること:
- Express の柔軟性と型定義の課題
- NestJS の構造化アプローチと型推論の強み
- 実務での選択基準
以下の図は、Express と NestJS のミドルウェア処理フローの違いを示しています。Express は手続き型でミドルウェアを順次実行するのに対し、NestJS はガード・インターセプター・パイプといった役割別のレイヤーで処理を分離します。
mermaidflowchart LR
reqE["リクエスト"] --> mw1["認証MW"]
mw1 --> mw2["ログMW"]
mw2 --> handler["ハンドラ"]
handler --> resE["レスポンス"]
reqN["リクエスト"] --> guard["ガード"]
guard --> interceptor["インターセプター"]
interceptor --> pipe["パイプ"]
pipe --> handlerN["ハンドラ"]
handlerN --> filter["フィルター"]
filter --> resN["レスポンス"]
Express は左側のシンプルなフロー、NestJS は右側の構造化されたフローです。
Express における型安全性の実現と課題
Express は軽量で柔軟ですが、TypeScript の型安全性を活かすには工夫が必要です。
Request オブジェクトの型拡張
Express で req.user を型安全に扱うには、以下のようにインターフェースを拡張します。
typescriptimport { Request, Response, NextFunction } from "express";
interface AuthUser {
id: string;
email: string;
role: "admin" | "user";
}
declare global {
namespace Express {
interface Request {
user?: AuthUser;
}
}
}
const authMiddleware = (
req: Request,
res: Response,
next: NextFunction,
): void => {
// 検証の結果、トークンが有効なら user を設定
req.user = {
id: "123",
email: "test@example.com",
role: "user",
};
next();
};
動作確認済み(Node.js 22.13.1、TypeScript 5.7.2)
この方法により、以降のミドルウェアやハンドラで req.user が型安全に扱えます。ただし、declare global を使うため、型定義の管理が散らばりやすい課題があります。
実際に試したところの結果
業務で Express の大規模プロジェクトに TypeScript を導入した際、declare global の型定義が複数ファイルに分散し、どこで req に何が追加されたか追跡困難になりました。この経験から、型定義を一元管理するファイル構成が重要だと学びました。
NestJS における型安全性の実現と強み
NestJS は、デコレーターと依存性注入(DI)により、型推論が自動で効きます。
デコレーターによる型安全な Request 拡張
typescriptimport { createParamDecorator, ExecutionContext } from "@nestjs/common";
interface AuthUser {
id: string;
email: string;
role: "admin" | "user";
}
export const CurrentUser = createParamDecorator(
(data: unknown, ctx: ExecutionContext): AuthUser => {
const request = ctx.switchToHttp().getRequest();
return request.user;
},
);
// コントローラーでの使用
import { Controller, Get } from "@nestjs/common";
@Controller("profile")
export class ProfileController {
@Get()
getProfile(@CurrentUser() user: AuthUser) {
// user は型推論が効いている
return { id: user.id, email: user.email };
}
}
動作確認済み(NestJS 10.4.15、TypeScript 5.7.2)
NestJS では、デコレーターが型情報を保持するため、declare global が不要です。型定義が局所化され、保守性が向上します。
実際に試したところの結果
NestJS プロジェクトでは、デコレーターにより型推論が自動で効き、IDE の補完機能が強力に働きました。ただし、デコレーターの仕組みを理解するまでの学習コストが高く、チーム全体で習得するまで時間を要しました。
つまずきポイント
- Express の
declare globalは名前空間汚染のリスクがあり、大規模プロジェクトでは管理が困難 - NestJS のデコレーターは強力だが、初学者には抽象度が高く感じられる
Express におけるミドルウェア設計パターンと型安全性
この章でわかること:
- 認証ミドルウェアの型安全な実装
- エラーハンドリングの統一パターン
- 実務での運用ノウハウ
認証ミドルウェアの設計と型安全性
Express で JWT 認証を実装する際、型安全性を保つには以下のパターンを用います。
typescriptimport jwt from "jsonwebtoken";
import { Request, Response, NextFunction } from "express";
interface JWTPayload {
userId: string;
email: string;
iat: number;
exp: number;
}
const authenticateToken = (
req: Request,
res: Response,
next: NextFunction,
): void => {
const authHeader = req.headers["authorization"];
const token = authHeader?.split(" ")[1];
if (!token) {
res.status(401).json({
error: "Access token required",
code: "AUTH_TOKEN_MISSING",
});
return;
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!) as JWTPayload;
req.user = {
id: decoded.userId,
email: decoded.email,
role: "user",
};
next();
} catch (error) {
if (error instanceof jwt.TokenExpiredError) {
res.status(401).json({
error: "Token expired",
code: "AUTH_TOKEN_EXPIRED",
});
return;
}
res.status(403).json({
error: "Invalid token",
code: "AUTH_TOKEN_INVALID",
});
}
};
動作確認済み(express 4.21.2、jsonwebtoken 9.0.2)
このパターンでは、エラーコード(AUTH_TOKEN_MISSING、AUTH_TOKEN_EXPIRED、AUTH_TOKEN_INVALID)を明示することで、フロントエンド側でのエラーハンドリングが容易になります。
検証の結果
実際の業務で、エラーコードを統一することで、フロントエンドとバックエンドのエラーハンドリング実装が効率化されました。ただし、初期にコード体系を設計せず、後から追加した結果、命名規則が不統一になり、修正に時間を要しました。
エラーハンドリングミドルウェアの統一パターン
Express のエラーミドルウェアは、4つの引数を持つ特殊な形式です。
typescriptinterface ApiError extends Error {
statusCode?: number;
code?: string;
details?: any;
}
const globalErrorHandler = (
error: ApiError,
req: Request,
res: Response,
next: NextFunction,
): void => {
const statusCode = error.statusCode || 500;
const errorCode = error.code || "INTERNAL_SERVER_ERROR";
console.error(
JSON.stringify({
level: "error",
message: error.message,
code: errorCode,
stack: error.stack,
timestamp: new Date().toISOString(),
}),
);
res.status(statusCode).json({
error: {
code: errorCode,
message: error.message,
...(process.env.NODE_ENV === "development" && {
stack: error.stack,
details: error.details,
}),
},
});
};
// app.js での使用
app.use(globalErrorHandler);
動作確認済み(express 4.21.2)
開発環境ではスタックトレースを含め、本番環境では除外することで、セキュリティと開発効率を両立します。
つまずきポイント
- エラーミドルウェアは必ず4つの引数を取る必要があり、3つにすると動作しない
next()を呼ばないと処理が止まるため、明示的に return する
NestJS におけるミドルウェア設計パターンと型安全性
この章でわかること:
- ガード・インターセプター・パイプ・フィルターの使い分け
- デコレーターによる型推論の活用
- 実務での設計判断
ガードによる認証・認可の実装
NestJS のガードは、CanActivate インターフェースを実装し、認証・認可を担います。
typescriptimport {
Injectable,
CanActivate,
ExecutionContext,
UnauthorizedException,
} from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException({
error: "Authentication required",
code: "AUTH_TOKEN_MISSING",
});
}
try {
const payload = await this.jwtService.verifyAsync(token);
request.user = payload;
return true;
} catch (error) {
if (error.name === "TokenExpiredError") {
throw new UnauthorizedException({
error: "Token expired",
code: "AUTH_TOKEN_EXPIRED",
});
}
throw new UnauthorizedException({
error: "Invalid token",
code: "AUTH_TOKEN_INVALID",
});
}
}
private extractTokenFromHeader(request: any): string | undefined {
const [type, token] = request.headers.authorization?.split(" ") ?? [];
return type === "Bearer" ? token : undefined;
}
}
動作確認済み(@nestjs/jwt 10.2.0)
ガードは依存性注入により JwtService を利用でき、コードが簡潔になります。
検証の結果
業務で NestJS のガードを採用した結果、認証ロジックが一箇所に集約され、テストが容易になりました。ただし、カスタムデコレーターとの組み合わせ方を理解するまで、試行錯誤が必要でした。
インターセプターによるログとレスポンス変換
インターセプターは、リクエスト・レスポンスの前後処理を担います。
typescriptimport {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from "@nestjs/common";
import { Observable } from "rxjs";
import { tap, map } from "rxjs/operators";
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const method = request.method;
const url = request.url;
const timestamp = new Date().toISOString();
console.log(`[${timestamp}] ${method} ${url} - START`);
const startTime = Date.now();
return next.handle().pipe(
tap(() => {
const duration = Date.now() - startTime;
console.log(`[${timestamp}] ${method} ${url} - END (${duration}ms)`);
}),
map((data) => ({
...data,
meta: {
timestamp,
duration: Date.now() - startTime,
},
})),
);
}
}
動作確認済み(@nestjs/common 10.4.15、rxjs 7.8.1)
RxJS の演算子により、非同期処理を宣言的に記述できます。
パイプによるバリデーションと型変換
パイプは、入力データの検証・変換を担います。
typescriptimport {
PipeTransform,
Injectable,
ArgumentMetadata,
BadRequestException,
} from "@nestjs/common";
@Injectable()
export class ValidationPipe implements PipeTransform<any> {
async transform(value: any, metadata: ArgumentMetadata) {
if (!value) {
throw new BadRequestException({
error: "Validation failed",
code: "VALIDATION_EMPTY_VALUE",
field: metadata.data,
});
}
if (metadata.type === "param" && metadata.data === "id") {
return this.validateId(value);
}
return value;
}
private validateId(value: string): string {
const uuidRegex =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRegex.test(value)) {
throw new BadRequestException({
error: "ID must be a valid UUID",
code: "VALIDATION_UUID_REQUIRED",
});
}
return value;
}
}
動作確認済み(@nestjs/common 10.4.15)
パイプにより、バリデーションロジックが再利用可能になります。
つまずきポイント
- インターセプターの RxJS 記法は、非同期処理に不慣れだと理解が難しい
- パイプは
PipeTransformを実装する必要があり、型定義を忘れるとエラーになる
Express vs NestJS の判断基準と比較まとめ(詳細版)
この章でわかること:
- どのような条件で Express / NestJS を選ぶか
- 型安全性・保守性・学習コストの観点での比較
- 実務での採用・不採用の理由
設計思想と型安全性の比較
| # | 観点 | Express | NestJS | 実務判断のポイント |
|---|---|---|---|---|
| 1 | 型定義方式 | declare global で手動拡張 | デコレーターで自動型推論 | 型推論重視なら NestJS |
| 2 | ミドルウェア構造 | 手続き型(関数チェーン) | 宣言型(デコレーター・DI) | 柔軟性重視なら Express |
| 3 | Request拡張 | インターフェース拡張が必要 | カスタムデコレーターで注入 | 拡張頻度が高いなら NestJS |
| 4 | エラーハンドリング | グローバルエラーミドルウェア | フィルター・例外レイヤー | 統一性重視なら NestJS |
| 5 | テスト容易性 | モック・スタブが必要 | DI でテストが容易 | テスト重視なら NestJS |
向いているケース・向かないケース
Express が向いているケース
- 小規模 API(エンドポイント数 10〜30 程度)
- プロトタイプ・MVP 開発でスピード重視
- チームに Express 経験者が多い
- 既存の Express プロジェクトへの段階的 TypeScript 導入
NestJS が向いているケース
- 大規模 API(エンドポイント数 50 以上)
- 長期運用・保守性重視
- 複雑なビジネスロジック・ドメイン駆動設計
- TypeScript ネイティブなチーム
実際に採用した・採用しなかった理由
Express を採用したケース:
業務で既存の Node.js プロジェクトに TypeScript を導入する際、学習コストを抑えるため Express を継続しました。ただし、declare global の管理が煩雑になり、途中で型定義ファイルを一元化する対応が必要でした。
NestJS を採用したケース: 新規プロジェクトで、複雑な認証・認可ロジックが求められたため、NestJS を採用しました。デコレーターと DI により、テストが容易で保守性が向上しました。ただし、チームの学習期間として 1〜2 週間を要しました。
つまずきポイント
- Express は型安全性の担保に工夫が必要で、設計ミスが後から響く
- NestJS は学習コストが高く、小規模プロジェクトではオーバースペック
まとめ
本記事では、TypeScript におけるミドルウェア設計を、Express と NestJS の比較を通じて解説しました。
- Express は柔軟性が高く、小規模・既存プロジェクトに向く
- NestJS は型安全性・保守性に優れ、大規模・長期運用に向く
- 型安全性を担保するには、Express では
declare global、NestJS ではデコレーターを活用 - エラーハンドリング・ログ・認証といった横断的関心事は、ミドルウェアで統一的に処理する
どちらが優れているかではなく、プロジェクトの規模・チームの習熟度・運用期間に応じて適切に選択することが重要です。実務では、型安全性と開発速度のバランスを取りながら、段階的に改善していく姿勢が求められます。
この記事が、あなたのミドルウェア設計の判断材料となれば幸いです。
関連リンク
著書
article2026年1月7日TypeScriptでミドルウェアを設計する ExpressとNestJSの応用例で型安全に整理する
articleNestJS デプロイ戦略:Blue-Green/Canary と DB マイグレーションの連携
articleNestJS 認可設計:RBAC/ABAC/ポリシーベース(CASL/oso)の実装指針
articleNestJS Guard/Interceptor/Filter 早見表:適用順序とユースケース対応表
articleNestJS Monorepo 構築:Nx/Yarn Workspaces で API・Lib を一元管理
articleNestJS × TypeORM vs Prisma vs Drizzle:DX・性能・移行性の総合ベンチ
article2026年1月10日TypeScriptでモノレポ管理をセットアップする手順 プロジェクト分割と依存関係制御の実践
article2026年1月10日StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する
article2026年1月9日TypeScriptプロジェクトの整形とLintをセットアップする手順 PrettierとESLintの最適構成
article2026年1月9日Vue 3とTypeScriptをセットアップして型安全に始める手順 propsとemitsの設計も整理
article2026年1月9日TypeScriptのビルド最適化を比較・検証する esbuild swc tscのベンチマークと使い分け
article2026年1月8日ESLintのparser設定を比較・検証する Babel TypeScript Flowの違いと選び方
article2026年1月10日TypeScriptでモノレポ管理をセットアップする手順 プロジェクト分割と依存関係制御の実践
article2026年1月10日StorybookとTypeScriptのユースケース 型安全なUI開発を設計して運用する
article2026年1月9日TypeScriptプロジェクトの整形とLintをセットアップする手順 PrettierとESLintの最適構成
article2026年1月9日Vue 3とTypeScriptをセットアップして型安全に始める手順 propsとemitsの設計も整理
article2026年1月9日TypeScriptのビルド最適化を比較・検証する esbuild swc tscのベンチマークと使い分け
article2026年1月8日ESLintのparser設定を比較・検証する Babel TypeScript Flowの違いと選び方
blogiPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
blogGoogleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
blog【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
blogGoogleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
blogPixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
blogフロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
review今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
reviewついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
review愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
review週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
review新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
review科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来
