自学内容网 自学内容网

NestJS 中间件与拦截器:请求处理流程详解

在上一篇文章中,我们介绍了 NestJS 的认证与授权实现。本文将深入探讨 NestJS 的请求处理流程,包括中间件、拦截器、管道和异常过滤器的使用。

请求生命周期

在 NestJS 中,请求处理流程按以下顺序执行:

  1. 中间件(Middleware)
  2. 守卫(Guards)
  3. 拦截器(Interceptors)- 前置处理
  4. 管道(Pipes)
  5. 控制器(Controller)
  6. 服务(Service)
  7. 拦截器(Interceptors)- 后置处理
  8. 异常过滤器(Exception Filters)

中间件实现

1. 函数式中间件

// src/common/middleware/logger.middleware.ts
import { Request, Response, NextFunction } from 'express';

export function loggerMiddleware(req: Request, res: Response, next: NextFunction) {
  const { method, originalUrl, ip } = req;
  const userAgent = req.get('user-agent') || '';

  console.log(`[${method}] ${originalUrl} - ${ip} - ${userAgent}`);

  // 记录请求开始时间
  const start = Date.now();

  // 响应结束后记录耗时
  res.on('finish', () => {
    const duration = Date.now() - start;
    console.log(`[${method}] ${originalUrl} - ${res.statusCode} - ${duration}ms`);
  });

  next();
}

2. 类中间件

// src/common/middleware/auth.middleware.ts
import { Injectable, NestMiddleware, UnauthorizedException } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  constructor(private jwtService: JwtService) {}

  async use(req: Request, res: Response, next: NextFunction) {
    const authHeader = req.headers.authorization;

    if (!authHeader) {
      next();
      return;
    }

    try {
      const token = authHeader.split(' ')[1];
      const payload = await this.jwtService.verifyAsync(token);
      req['user'] = payload;
      next();
    } catch (error) {
      throw new UnauthorizedException('Invalid token');
    }
  }
}

3. 全局中间件

// src/app.module.ts
import { Module, NestModule, MiddlewareConsumer, RequestMethod } from '@nestjs/common';
import { AuthMiddleware } from './common/middleware/auth.middleware';
import { loggerMiddleware } from './common/middleware/logger.middleware';

@Module({
  // ... 其他配置
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(loggerMiddleware, AuthMiddleware)
      .exclude(
        { path: 'auth/login', method: RequestMethod.POST },
        { path: 'auth/register', method: RequestMethod.POST },
      )
      .forRoutes('*');
  }
}

拦截器实现

1. 响应转换拦截器

// src/common/interceptors/transform.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Response<T> {
  data: T;
  meta: {
    timestamp: string;
    status: number;
    message: string;
  };
}

@Injectable()
export class TransformInterceptor<T>
  implements NestInterceptor<T, Response<T>> {
  intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Observable<Response<T>> {
    const ctx = context.switchToHttp();
    const response = ctx.getResponse();

    return next.handle().pipe(
      map(data => ({
        data,
        meta: {
          timestamp: new Date().toISOString(),
          status: response.statusCode,
          message: 'Success',
        },
      })),
    );
  }
}

2. 缓存拦截器

// src/common/interceptors/cache.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { RedisService } from '../services/redis.service';

@Injectable()
export class CacheInterceptor implements NestInterceptor {
  constructor(private redisService: RedisService) {}

  async intercept(
    context: ExecutionContext,
    next: CallHandler,
  ): Promise<Observable<any>> {
    const request = context.switchToHttp().getRequest();
    const cacheKey = `cache:${request.url}`;

    // 尝试从缓存获取数据
    const cachedData = await this.redisService.get(cacheKey);
    if (cachedData) {
      return of(JSON.parse(cachedData));
    }

    // 如果没有缓存,执行请求并缓存结果
    return next.handle().pipe(
      tap(async response => {
        await this.redisService.set(
          cacheKey,
          JSON.stringify(response),
          60 * 5, // 5分钟缓存
        );
      }),
    );
  }
}

3. 性能监控拦截器

// src/common/interceptors/logging.interceptor.ts
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';
import { PrometheusService } from '../services/prometheus.service';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  constructor(private prometheusService: PrometheusService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const { method, url } = request;
    const start = Date.now();

    return next.handle().pipe(
      tap({
        next: () => {
          const duration = Date.now() - start;

          // 记录请求耗时
          this.prometheusService.recordHttpRequestDuration(
            method,
            url,
            duration,
          );
        },
        error: error => {
          const duration = Date.now() - start;

          // 记录错误请求
          this.prometheusService.recordHttpRequestError(
            method,
            url,
            error.status,
            duration,
          );
        },
      }),
    );
  }
}

管道实现

1. 验证管道

// src/common/pipes/validation.pipe.ts
import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';
import { validate } from 'class-validator';
import { plainToClass } from 'class-transformer';

@Injectable()
export class ValidationPipe implements PipeTransform<any> {
  async transform(value: any, { metatype }: ArgumentMetadata) {
    if (!metatype || !this.toValidate(metatype)) {
      return value;
    }

    const object = plainToClass(metatype, value);
    const errors = await validate(object);

    if (errors.length > 0) {
      const messages = errors.map(error => ({
        property: error.property,
        constraints: error.constraints,
      }));

      throw new BadRequestException({
        message: 'Validation failed',
        errors: messages,
      });
    }

    return value;
  }

  private toValidate(metatype: Function): boolean {
    const types: Function[] = [String, Boolean, Number, Array, Object];
    return !types.includes(metatype);
  }
}

2. 转换管道

// src/common/pipes/parse-int.pipe.ts
import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  BadRequestException,
} from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException(
        `Validation failed. "${value}" is not an integer.`,
      );
    }
    return val;
  }
}

// src/common/pipes/parse-boolean.pipe.ts
@Injectable()
export class ParseBooleanPipe implements PipeTransform<string, boolean> {
  transform(value: string, metadata: ArgumentMetadata): boolean {
    if (value === 'true') return true;
    if (value === 'false') return false;
    throw new BadRequestException(
      `Validation failed. "${value}" is not a boolean.`,
    );
  }
}

异常过滤器

1. 全局异常过滤器

// src/common/filters/http-exception.filter.ts
import {
  ExceptionFilter,
  Catch,
  ArgumentsHost,
  HttpException,
  HttpStatus,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { LoggerService } from '../services/logger.service';

@Catch()
export class HttpExceptionFilter implements ExceptionFilter {
  constructor(private logger: LoggerService) {}

  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 message =
      exception instanceof HttpException
        ? exception.getResponse()
        : 'Internal server error';

    // 记录错误日志
    this.logger.error(
      `${request.method} ${request.url}`,
      exception instanceof Error ? exception.stack : 'Unknown error',
      'HttpExceptionFilter',
    );

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message,
    });
  }
}

2. 业务异常过滤器

// src/common/filters/business-exception.filter.ts
import { Catch, ArgumentsHost, HttpStatus } from '@nestjs/common';
import { BaseExceptionFilter } from '@nestjs/core';
import { BusinessException } from '../exceptions/business.exception';
import { LoggerService } from '../services/logger.service';

@Catch(BusinessException)
export class BusinessExceptionFilter extends BaseExceptionFilter {
  constructor(private logger: LoggerService) {
    super();
  }

  catch(exception: BusinessException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse();
    const request = ctx.getRequest();

    const status = HttpStatus.BAD_REQUEST;

    // 记录业务错误日志
    this.logger.warn(
      `Business Exception: ${exception.message}`,
      exception.stack,
      'BusinessExceptionFilter',
    );

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      error: 'Business Error',
      message: exception.message,
      code: exception.code,
    });
  }
}

实践应用

1. 请求链路追踪

// src/common/middleware/trace.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';

@Injectable()
export class TraceMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const traceId = req.headers['x-trace-id'] || uuidv4();
    req['traceId'] = traceId;
    res.setHeader('X-Trace-Id', traceId);
    next();
  }
}

// src/common/interceptors/trace.interceptor.ts
@Injectable()
export class TraceInterceptor implements NestInterceptor {
  constructor(private logger: LoggerService) {}

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const request = context.switchToHttp().getRequest();
    const traceId = request['traceId'];

    return next.handle().pipe(
      tap(data => {
        this.logger.log(
          `[${traceId}] Response data: ${JSON.stringify(data)}`,
          'TraceInterceptor',
        );
      }),
    );
  }
}

2. 请求速率限制

// src/common/guards/throttler.guard.ts
import { Injectable } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';

@Injectable()
export class CustomThrottlerGuard extends ThrottlerGuard {
  protected getTracker(req: Record<string, any>): string {
    return req.ips.length ? req.ips[0] : req.ip;
  }
}

// src/app.module.ts
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      ttl: 60,
      limit: 10,
    }),
  ],
})
export class AppModule {}

3. API 版本控制

// src/common/decorators/api-version.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const API_VERSION = 'api_version';
export const ApiVersion = (version: string) => SetMetadata(API_VERSION, version);

// src/common/guards/api-version.guard.ts
@Injectable()
export class ApiVersionGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const version = this.reflector.get<string>(
      API_VERSION,
      context.getHandler(),
    );

    if (!version) {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const requestVersion = request.headers['api-version'];

    return version === requestVersion;
  }
}

写在最后

本文详细介绍了 NestJS 中的请求处理流程和各个组件的实现:

  1. 中间件的不同实现方式
  2. 拦截器的应用场景
  3. 管道的数据转换和验证
  4. 异常过滤器的错误处理
  5. 实际应用案例

在下一篇文章中,我们将探讨 NestJS 的微服务架构实现。

如果觉得这篇文章对你有帮助,别忘了点个赞 👍


原文地址:https://blog.csdn.net/ChengFengTech/article/details/144795251

免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!