T-CREATOR

TypeScript で学ぶミドルウェア設計パターン:Express・NestJS の応用例

TypeScript で学ぶミドルウェア設計パターン:Express・NestJS の応用例

モダンな Web アプリケーション開発において、ミドルウェアは縁の下の力持ちのような存在です。あなたが普段使っている Web サービスで、ログイン状態をチェックしたり、API のレスポンス時間を記録したり、セキュリティを守ったりしている機能の多くが、実はミドルウェアによって支えられています。

しかし、多くの開発者がミドルウェアを「なんとなく」で実装してしまい、保守性や拡張性に課題を抱えているのも事実です。TypeScript を活用することで、型安全性を保ちながら、より堅牢で美しいミドルウェア設計が可能になります。

本記事では、Express.js と NestJS という 2 つの人気フレームワークを通じて、実際の現場で使える具体的なミドルウェア設計パターンをお伝えします。エラーコードも含めた実践的な内容で、あなたのアプリケーション開発に新たな視点をもたらすことでしょう。

ミドルウェア設計パターンの基礎知識

ミドルウェアとは何か

ミドルウェアは、リクエストとレスポンスの間に挟まる処理のことです。まるで料理における「下ごしらえ」のような役割を果たします。

typescript// ミドルウェアの基本構造
interface MiddlewareFunction {
  (req: Request, res: Response, next: NextFunction): void;
}

// 実際の使用例
const basicMiddleware: MiddlewareFunction = (
  req,
  res,
  next
) => {
  console.log('リクエストを受信しました');
  next(); // 次の処理に制御を移す
};

このnext()関数の呼び出しが、ミドルウェアチェーンを形成する重要なポイントです。まるでバトンリレーのように、一つずつ処理が引き継がれていきます。

TypeScript でのミドルウェア実装の利点

TypeScript を使うことで、ミドルウェアの開発は劇的に改善されます。型安全性により、実行時エラーを事前に防げるのです。

#利点説明
1型安全性コンパイル時にエラーを検出
2インテリセンスIDE での補完機能が充実
3リファクタリング安全な構造変更が可能
4ドキュメント化型定義自体がドキュメントの役割
typescript// 型安全なリクエスト拡張
interface AuthenticatedRequest extends Request {
  user?: {
    id: string;
    email: string;
    role: 'admin' | 'user';
  };
}

// このように型を指定することで、user プロパティの存在が保証される
const authMiddleware = (
  req: AuthenticatedRequest,
  res: Response,
  next: NextFunction
) => {
  // TypeScriptにより、ここでuserの型が保証される
  if (req.user?.role === 'admin') {
    next();
  } else {
    res.status(403).json({ error: 'Forbidden' });
  }
};

Express.js でのミドルウェア設計パターン

Express.js は、シンプルで直感的なミドルウェア設計を可能にします。ここでは、実際の現場でよく使われる 4 つのパターンをご紹介します。

認証ミドルウェアパターン

認証は、Web アプリケーションのセキュリティの要です。以下は、JWT トークンを使った認証ミドルウェアの実装例です。

typescriptimport jwt from 'jsonwebtoken';

interface JWTPayload {
  userId: string;
  email: string;
  iat: number;
  exp: number;
}

const authenticateToken = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

  if (!token) {
    return res.status(401).json({
      error: 'Access token required',
      code: 'AUTH_TOKEN_MISSING'
    });
  }

このパターンでは、リクエストヘッダーからトークンを抽出し、その有効性を検証します。実際のエラーコードAUTH_TOKEN_MISSINGを含めることで、フロントエンド側での適切なエラーハンドリングが可能になります。

typescript  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) {
      return res.status(401).json({
        error: 'Token expired',
        code: 'AUTH_TOKEN_EXPIRED'
      });
    }

    return res.status(403).json({
      error: 'Invalid token',
      code: 'AUTH_TOKEN_INVALID'
    });
  }
};

この実装により、トークンの有効期限切れ(AUTH_TOKEN_EXPIRED)と無効なトークン(AUTH_TOKEN_INVALID)を明確に区別できます。これは、ユーザー体験の向上に直結する重要な設計です。

ログ取得ミドルウェアパターン

アプリケーションの監視と問題解決において、ログは欠かせない要素です。構造化されたログを取得することで、運用時のトラブルシューティングが格段に楽になります。

typescriptimport { v4 as uuidv4 } from 'uuid';

interface LogContext {
  requestId: string;
  method: string;
  url: string;
  userAgent?: string;
  ip: string;
  timestamp: Date;
}

interface RequestWithContext extends Request {
  context: LogContext;
}

const requestLogger = (
  req: RequestWithContext,
  res: Response,
  next: NextFunction
) => {
  const requestId = uuidv4();
  const startTime = Date.now();

  // リクエストコンテキストを設定
  req.context = {
    requestId,
    method: req.method,
    url: req.url,
    userAgent: req.get('User-Agent'),
    ip: req.ip,
    timestamp: new Date()
  };

このパターンでは、各リクエストにユニークな ID を付与し、追跡可能な構造を作ります。requestIdにより、分散システムでもリクエストの流れを追跡できるようになります。

typescript  // リクエスト開始をログに記録
  console.log(JSON.stringify({
    level: 'info',
    message: 'Request started',
    requestId: req.context.requestId,
    method: req.method,
    url: req.url,
    userAgent: req.context.userAgent,
    timestamp: req.context.timestamp.toISOString()
  }));

  // レスポンス終了時の処理
  res.on('finish', () => {
    const duration = Date.now() - startTime;
    const logLevel = res.statusCode >= 400 ? 'error' : 'info';

    console.log(JSON.stringify({
      level: logLevel,
      message: 'Request completed',
      requestId: req.context.requestId,
      statusCode: res.statusCode,
      duration: `${duration}ms`,
      timestamp: new Date().toISOString()
    }));
  });

  next();
};

このログ形式により、パフォーマンスの問題やエラーの発生箇所を素早く特定できます。特に、レスポンス時間が長いリクエストや、エラーが発生したリクエストを効率的に見つけることが可能です。

エラーハンドリングミドルウェアパターン

エラーハンドリングは、アプリケーションの信頼性を決定する重要な要素です。Express.js では、エラーミドルウェアは 4 つの引数を持つ特別な形式で実装します。

typescriptinterface ApiError extends Error {
  statusCode?: number;
  code?: string;
  details?: any;
}

const globalErrorHandler = (
  error: ApiError,
  req: RequestWithContext,
  res: Response,
  next: NextFunction
) => {
  // デフォルトのエラー情報を設定
  const statusCode = error.statusCode || 500;
  const errorCode = error.code || 'INTERNAL_SERVER_ERROR';
  const message = error.message || 'An unexpected error occurred';

  // エラーログを記録
  console.error(JSON.stringify({
    level: 'error',
    message: 'Unhandled error',
    requestId: req.context?.requestId,
    error: {
      name: error.name,
      message: error.message,
      stack: error.stack,
      code: errorCode
    },
    timestamp: new Date().toISOString()
  }));

このパターンでは、エラーの種類に応じて適切なレスポンスを返します。開発環境では詳細なエラー情報を、本番環境では安全な情報のみを返すように制御できます。

typescript  // 開発環境と本番環境でエラー情報を切り替え
  const errorResponse = {
    error: {
      code: errorCode,
      message: message,
      requestId: req.context?.requestId,
      timestamp: new Date().toISOString(),
      // 開発環境でのみスタックトレースを含める
      ...(process.env.NODE_ENV === 'development' && {
        stack: error.stack,
        details: error.details
      })
    }
  };

  // 特定のエラーコードに応じた処理
  if (errorCode === 'VALIDATION_ERROR') {
    return res.status(400).json(errorResponse);
  }

  if (errorCode === 'PERMISSION_DENIED') {
    return res.status(403).json(errorResponse);
  }

  res.status(statusCode).json(errorResponse);
};

このような統一されたエラーハンドリングにより、フロントエンド側でも一貫したエラー処理が可能になります。

CORS 対応ミドルウェアパターン

現代の Web アプリケーションでは、異なるオリジンからの API アクセスが一般的です。CORS(Cross-Origin Resource Sharing)の適切な設定は、セキュリティと利便性のバランスを取る重要な要素です。

typescriptinterface CorsOptions {
  allowedOrigins: string[];
  allowedMethods: string[];
  allowedHeaders: string[];
  credentials: boolean;
  maxAge?: number;
}

const corsMiddleware = (options: CorsOptions) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const origin = req.headers.origin;

    // オリジンの検証
    if (origin && options.allowedOrigins.includes(origin)) {
      res.header('Access-Control-Allow-Origin', origin);
    } else if (options.allowedOrigins.includes('*')) {
      res.header('Access-Control-Allow-Origin', '*');
    }

    // 認証情報の送信を許可
    if (options.credentials) {
      res.header('Access-Control-Allow-Credentials', 'true');
    }

このパターンでは、設定可能なオプションを通じて、柔軟な CORS 設定を実現します。セキュリティを重視しながら、必要な機能は適切に許可する設計です。

typescript    // 許可するメソッドとヘッダーを設定
    res.header(
      'Access-Control-Allow-Methods',
      options.allowedMethods.join(', ')
    );

    res.header(
      'Access-Control-Allow-Headers',
      options.allowedHeaders.join(', ')
    );

    // プリフライトリクエストの最大キャッシュ時間
    if (options.maxAge) {
      res.header('Access-Control-Max-Age', options.maxAge.toString());
    }

    // OPTIONS リクエスト(プリフライト)の処理
    if (req.method === 'OPTIONS') {
      return res.status(200).end();
    }

    next();
  };
};

// 使用例
app.use(corsMiddleware({
  allowedOrigins: ['https://example.com', 'https://app.example.com'],
  allowedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400 // 24時間
}));

このような設定により、セキュリティを保ちながら、必要なクロスオリジンアクセスを適切に許可できます。

NestJS でのミドルウェア設計パターン

NestJS は、より高度で構造化されたミドルウェア設計を提供します。デコレーターと依存性注入を活用することで、よりエレガントで保守性の高いコードを書くことができます。

ガードパターン

ガードは、リクエストが特定の条件を満たすかどうかを判定し、ルートハンドラーの実行を制御します。認可処理に特化した設計パターンです。

typescriptimport {
  Injectable,
  CanActivate,
  ExecutionContext,
  UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(
    context: ExecutionContext
  ): Promise<boolean> {
    const request = context
      .switchToHttp()
      .getRequest<Request>();
    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: Request
  ): string | undefined {
    const [type, token] =
      request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

このガードパターンでは、CanActivateインターフェースを実装することで、認証ロジックをカプセル化しています。依存性注入により、JwtServiceを簡潔に利用できます。

typescript// 使用例
import { Controller, Get, UseGuards } from '@nestjs/common';

@Controller('users')
@UseGuards(AuthGuard)
export class UsersController {
  @Get()
  findAll() {
    return { message: 'Authenticated users only' };
  }

  @Get('profile')
  getProfile(@Request() req) {
    return req.user;
  }
}

インターセプターパターン

インターセプターは、リクエストの前後で処理を実行できる強力なパターンです。ロギング、データ変換、キャッシュなど、横断的な関心事を効率的に処理できます。

typescriptimport {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/core';
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,
        },
      }))
    );
  }
}

このインターセプターは、リクエストの開始と終了をログに記録し、レスポンスにメタデータを追加します。RxJS の演算子を活用することで、非同期処理も美しく扱えます。

typescript// エラーハンドリングを含む高度なインターセプター
import { catchError, throwError } from 'rxjs';

@Injectable()
export class ErrorHandlingInterceptor
  implements NestInterceptor
{
  intercept(
    context: ExecutionContext,
    next: CallHandler
  ): Observable<any> {
    return next.handle().pipe(
      catchError((error) => {
        const request = context.switchToHttp().getRequest();
        const errorLog = {
          timestamp: new Date().toISOString(),
          method: request.method,
          url: request.url,
          error: {
            name: error.name,
            message: error.message,
            stack: error.stack,
          },
        };

        console.error(
          'Request failed:',
          JSON.stringify(errorLog)
        );

        return throwError(() => error);
      })
    );
  }
}

パイプパターン

パイプは、入力データの変換と検証を行うパターンです。リクエストデータが期待する形式になっているかを確認し、必要に応じて変換を行います。

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);
    }

    if (metadata.type === 'body') {
      return this.validateBody(value, metadata);
    }

    return value;
  }

  private validateId(value: string): string {
    if (!value || typeof value !== 'string') {
      throw new BadRequestException({
        error: 'Invalid ID format',
        code: 'VALIDATION_INVALID_ID'
      });
    }

    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;
  }

このパイプでは、ID の形式検証を行い、適切なエラーコードを返します。VALIDATION_INVALID_IDVALIDATION_UUID_REQUIREDなどの具体的なエラーコードにより、フロントエンド側での細かいエラーハンドリングが可能になります。

typescript  private validateBody(value: any, metadata: ArgumentMetadata) {
    if (typeof value !== 'object' || Array.isArray(value)) {
      throw new BadRequestException({
        error: 'Request body must be an object',
        code: 'VALIDATION_INVALID_BODY_TYPE'
      });
    }

    // 必須フィールドの検証
    const requiredFields = ['name', 'email'];
    for (const field of requiredFields) {
      if (!(field in value) || !value[field]) {
        throw new BadRequestException({
          error: `Field '${field}' is required`,
          code: 'VALIDATION_REQUIRED_FIELD',
          field: field
        });
      }
    }

    // メールアドレスの形式検証
    if (value.email && !this.isValidEmail(value.email)) {
      throw new BadRequestException({
        error: 'Invalid email format',
        code: 'VALIDATION_INVALID_EMAIL'
      });
    }

    return value;
  }

  private isValidEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }
}

フィルターパターン

フィルターは、例外処理を統一的に扱うパターンです。アプリケーション全体で一貫したエラーレスポンスを提供できます。

typescriptimport { ExceptionFilter, Catch, ArgumentsHost, HttpException, HttpStatus } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  catch(exception: unknown, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();

    const status = exception instanceof HttpException
      ? exception.getStatus()
      : HttpStatus.INTERNAL_SERVER_ERROR;

    const errorResponse = this.getErrorResponse(exception, request, status);

    // エラーログを記録
    console.error(JSON.stringify({
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method,
      statusCode: status,
      error: errorResponse,
      stack: exception instanceof Error ? exception.stack : undefined
    }));

    response.status(status).json(errorResponse);
  }

このフィルターでは、あらゆる例外をキャッチし、統一された形式でエラーレスポンスを返します。ログ記録も同時に行うことで、問題の追跡が容易になります。

typescript  private getErrorResponse(exception: unknown, request: Request, status: number) {
    const baseResponse = {
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      method: request.method
    };

    if (exception instanceof HttpException) {
      const response = exception.getResponse();
      return {
        ...baseResponse,
        ...(typeof response === 'object' ? response : { message: response })
      };
    }

    // 予期しないエラーの場合
    return {
      ...baseResponse,
      error: 'Internal server error',
      code: 'INTERNAL_SERVER_ERROR',
      message: process.env.NODE_ENV === 'development'
        ? (exception as Error).message
        : 'An unexpected error occurred'
    };
  }
}

// 使用例
import { APP_FILTER } from '@nestjs/core';

@Module({
  providers: [
    {
      provide: APP_FILTER,
      useClass: AllExceptionsFilter,
    },
  ],
})
export class AppModule {}

これらの NestJS パターンを組み合わせることで、非常に堅牢で保守性の高いミドルウェアアーキテクチャを構築できます。

実践的な応用例とベストプラクティス

ここまで学んだパターンを実際のプロジェクトで活用するための、より実践的な応用例をご紹介します。セキュリティとパフォーマンスを両立させながら、保守性の高いアーキテクチャを構築しましょう。

リクエスト検証とサニタイゼーション

ユーザーからの入力データは、常に疑ってかかる必要があります。適切な検証とサニタイゼーションにより、SQL インジェクションや XSS 攻撃を防げます。

typescriptimport {
  body,
  param,
  validationResult,
} from 'express-validator';
import DOMPurify from 'dompurify';
import { JSDOM } from 'jsdom';

// サニタイゼーション用の設定
const window = new JSDOM('').window;
const purify = DOMPurify(window);

interface SanitizedRequest extends Request {
  sanitizedBody?: any;
  sanitizedParams?: any;
}

const sanitizeMiddleware = (
  req: SanitizedRequest,
  res: Response,
  next: NextFunction
) => {
  // リクエストボディのサニタイゼーション
  if (req.body && typeof req.body === 'object') {
    req.sanitizedBody = sanitizeObject(req.body);
  }

  // パラメータのサニタイゼーション
  if (req.params && typeof req.params === 'object') {
    req.sanitizedParams = sanitizeObject(req.params);
  }

  next();
};

const sanitizeObject = (obj: any): any => {
  if (typeof obj === 'string') {
    return purify.sanitize(obj);
  }

  if (Array.isArray(obj)) {
    return obj.map(sanitizeObject);
  }

  if (obj && typeof obj === 'object') {
    const sanitized: any = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        sanitized[key] = sanitizeObject(obj[key]);
      }
    }
    return sanitized;
  }

  return obj;
};

このサニタイゼーションミドルウェアは、再帰的にオブジェクトを処理し、悪意のあるスクリプトを除去します。DOMPurifyを使用することで、確実な HTML サニタイゼーションを実現できます。

typescript// 実際の使用例
const userValidation = [
  body('email')
    .isEmail()
    .withMessage('Valid email is required')
    .normalizeEmail(),
  body('password')
    .isLength({ min: 8 })
    .withMessage('Password must be at least 8 characters')
    .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/)
    .withMessage(
      'Password must contain uppercase, lowercase, and number'
    ),
  body('name')
    .isLength({ min: 2, max: 50 })
    .withMessage('Name must be between 2 and 50 characters')
    .trim()
    .escape(),
];

const handleValidationErrors = (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  const errors = validationResult(req);
  if (!errors.isEmpty()) {
    return res.status(400).json({
      error: 'Validation failed',
      code: 'VALIDATION_ERROR',
      details: errors.array().map((error) => ({
        field: error.param,
        message: error.msg,
        value: error.value,
      })),
    });
  }
  next();
};

// 使用例
app.post(
  '/api/users',
  sanitizeMiddleware,
  userValidation,
  handleValidationErrors,
  createUserController
);

レート制限とセキュリティ強化

API の濫用を防ぐため、レート制限は必須の機能です。Redis を使用した分散対応のレート制限機能を実装してみましょう。

typescriptimport Redis from 'ioredis';

interface RateLimitOptions {
  windowMs: number;
  max: number;
  message: string;
  skipSuccessfulRequests?: boolean;
  skipFailedRequests?: boolean;
}

class RateLimiter {
  private redis: Redis;

  constructor(redisUrl: string) {
    this.redis = new Redis(redisUrl);
  }

  createMiddleware(options: RateLimitOptions) {
    return async (
      req: Request,
      res: Response,
      next: NextFunction
    ) => {
      const key = this.generateKey(req);
      const window = Math.floor(
        Date.now() / options.windowMs
      );
      const redisKey = `rate_limit:${key}:${window}`;

      try {
        const current = await this.redis.incr(redisKey);

        if (current === 1) {
          await this.redis.expire(
            redisKey,
            Math.ceil(options.windowMs / 1000)
          );
        }

        // レスポンスヘッダーに制限情報を追加
        res.set({
          'X-RateLimit-Limit': options.max.toString(),
          'X-RateLimit-Remaining': Math.max(
            0,
            options.max - current
          ).toString(),
          'X-RateLimit-Reset': new Date(
            (window + 1) * options.windowMs
          ).toISOString(),
        });

        if (current > options.max) {
          return res.status(429).json({
            error: options.message,
            code: 'RATE_LIMIT_EXCEEDED',
            retryAfter: Math.ceil(options.windowMs / 1000),
          });
        }

        next();
      } catch (error) {
        console.error('Rate limiter error:', error);
        next(); // Redis エラーの場合は通す
      }
    };
  }

  private generateKey(req: Request): string {
    // IP アドレスとユーザー ID を組み合わせてキーを生成
    const ip = req.ip || req.connection.remoteAddress;
    const userId = (req as any).user?.id;
    return userId ? `user:${userId}` : `ip:${ip}`;
  }
}

この実装では、Redis を使用してレート制限情報を共有し、複数のサーバーインスタンス間で一貫した制限を適用できます。

typescript// 使用例
const rateLimiter = new RateLimiter(process.env.REDIS_URL!);

// 一般的な API 制限
app.use(
  '/api',
  rateLimiter.createMiddleware({
    windowMs: 15 * 60 * 1000, // 15分
    max: 100, // 100リクエスト
    message: 'Too many requests from this IP',
  })
);

// ログイン API の厳しい制限
app.use(
  '/api/auth/login',
  rateLimiter.createMiddleware({
    windowMs: 15 * 60 * 1000, // 15分
    max: 5, // 5回まで
    message: 'Too many login attempts',
  })
);

パフォーマンス監視

アプリケーションのパフォーマンスを継続的に監視することで、問題を早期発見できます。メトリクス収集とアラート機能を備えた監視システムを構築しましょう。

typescriptimport { performance } from 'perf_hooks';

interface PerformanceMetrics {
  requestCount: number;
  errorCount: number;
  averageResponseTime: number;
  p95ResponseTime: number;
  memoryUsage: NodeJS.MemoryUsage;
}

class PerformanceMonitor {
  private metrics: Map<string, number[]> = new Map();
  private counters: Map<string, number> = new Map();
  private alertThresholds: Map<string, number> = new Map();

  constructor() {
    this.setupAlerts();
    this.startPeriodicReporting();
  }

  createMiddleware() {
    return (req: Request, res: Response, next: NextFunction) => {
      const startTime = performance.now();
      const endpoint = `${req.method} ${req.route?.path || req.path}`;

      this.incrementCounter('total_requests');
      this.incrementCounter(`requests_${endpoint}`);

      res.on('finish', () => {
        const endTime = performance.now();
        const responseTime = endTime - startTime;

        this.recordMetric('response_time', responseTime);
        this.recordMetric(`response_time_${endpoint}`, responseTime);

        if (res.statusCode >= 400) {
          this.incrementCounter('error_requests');
          this.incrementCounter(`errors_${endpoint}`);
        }

        // 異常に遅いリクエストをアラート
        if (responseTime > 5000) { // 5秒以上
          this.triggerAlert('SLOW_REQUEST', {
            endpoint,
            responseTime,
            statusCode: res.statusCode
          });
        }
      });

      next();
    };
  }

  private recordMetric(key: string, value: number) {
    if (!this.metrics.has(key)) {
      this.metrics.set(key, []);
    }
    const values = this.metrics.get(key)!;
    values.push(value);

    // 直近100件のみ保持
    if (values.length > 100) {
      values.shift();
    }
  }

  private incrementCounter(key: string) {
    this.counters.set(key, (this.counters.get(key) || 0) + 1);
  }

このパフォーマンスモニターは、リクエストの応答時間、エラー率、メモリ使用量などの重要な指標を追跡します。

typescript  private calculatePercentile(values: number[], percentile: number): number {
    const sorted = values.slice().sort((a, b) => a - b);
    const index = Math.ceil(sorted.length * percentile / 100) - 1;
    return sorted[index] || 0;
  }

  private getCurrentMetrics(): PerformanceMetrics {
    const responseTimes = this.metrics.get('response_time') || [];
    const totalRequests = this.counters.get('total_requests') || 0;
    const errorRequests = this.counters.get('error_requests') || 0;

    return {
      requestCount: totalRequests,
      errorCount: errorRequests,
      averageResponseTime: responseTimes.length > 0
        ? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
        : 0,
      p95ResponseTime: this.calculatePercentile(responseTimes, 95),
      memoryUsage: process.memoryUsage()
    };
  }

  private setupAlerts() {
    this.alertThresholds.set('error_rate', 5); // 5%
    this.alertThresholds.set('average_response_time', 1000); // 1秒
    this.alertThresholds.set('memory_usage', 500 * 1024 * 1024); // 500MB
  }

  private triggerAlert(type: string, details: any) {
    const alert = {
      type,
      details,
      timestamp: new Date().toISOString(),
      severity: 'warning'
    };

    console.warn('PERFORMANCE ALERT:', JSON.stringify(alert));

    // 実際のプロダクションでは、Slack や PagerDuty に通知
    // await this.sendToSlack(alert);
  }

  private startPeriodicReporting() {
    setInterval(() => {
      const metrics = this.getCurrentMetrics();
      console.log('Performance Metrics:', JSON.stringify(metrics, null, 2));

      // アラートチェック
      const errorRate = metrics.errorCount / metrics.requestCount * 100;
      if (errorRate > this.alertThresholds.get('error_rate')!) {
        this.triggerAlert('HIGH_ERROR_RATE', { errorRate });
      }

      if (metrics.averageResponseTime > this.alertThresholds.get('average_response_time')!) {
        this.triggerAlert('SLOW_RESPONSE_TIME', {
          averageResponseTime: metrics.averageResponseTime
        });
      }
    }, 60000); // 1分間隔
  }
}

// 使用例
const monitor = new PerformanceMonitor();
app.use(monitor.createMiddleware());

これらの実践的なパターンを組み合わせることで、本格的なプロダクションレベルのミドルウェアシステムを構築できます。

まとめ

本記事では、TypeScript を活用したミドルウェア設計パターンについて、Express.js と NestJS の両方のアプローチを詳しく解説しました。

あなたが身につけた知識

基礎理論の理解

  • ミドルウェアの本質と動作原理
  • TypeScript による型安全性の恩恵
  • リクエスト・レスポンスサイクルでの処理の流れ

Express.js でのパターン習得

  • 認証ミドルウェアによるセキュリティ実装
  • 構造化ログによる運用効率化
  • エラーハンドリングの統一化手法
  • CORS 設定によるクロスオリジン対応

NestJS でのモダンアーキテクチャ

  • デコレーターベースの宣言的設計
  • ガード、インターセプター、パイプ、フィルターの使い分け
  • 依存性注入による保守性の向上
  • RxJS を活用した非同期処理の美しい実装

実践的な応用技術

  • 入力検証とサニタイゼーションによるセキュリティ強化
  • Redis を使った分散レート制限システム
  • パフォーマンス監視とアラート機能

実際の開発で活かすためのポイント

#重要なポイント実践方法
1型安全性を最優先にインターフェースを定義し、型チェックを徹底する
2エラーコードの統一一貫したエラーコード体系を設計する
3ログの構造化JSON 形式での統一されたログ出力
4セキュリティの多層防御認証、認可、検証、サニタイゼーションを組み合わせる
5パフォーマンスの継続的監視メトリクス収集とアラート機能を実装する

次のステップへの道筋

この記事で学んだ内容を基に、さらなる高度な実装に挑戦してみてください。

typescript// 発展的な実装例:複数のミドルウェアを組み合わせた統合システム
const createSecureAPIMiddleware = () => {
  return [
    sanitizeMiddleware,
    rateLimiter.createMiddleware({
      windowMs: 15 * 60 * 1000,
      max: 100,
      message: 'Too many requests',
    }),
    monitor.createMiddleware(),
    authenticateToken,
    globalErrorHandler,
  ];
};

// 使用例
app.use('/api/secure', createSecureAPIMiddleware());

あなたのアプリケーションが、ユーザーにとって安全で快適な体験を提供し、開発チームにとって保守しやすいシステムになることを願っています。ミドルウェアの設計は、まさに「縁の下の力持ち」として、アプリケーション全体の品質を支える重要な要素です。

この記事が、あなたの技術的な成長と、より良いソフトウェア開発の一助となれば幸いです。実際のプロジェクトでこれらのパターンを活用し、さらなる改善を重ねていってください。

関連リンク

公式ドキュメント

ライブラリ・ツール

セキュリティ関連

パフォーマンス・監視