从零入门 NestJS:构建你的第一个 REST API(七)异常处理与日志记录

218 阅读5分钟

异常处理与日志记录

在后端开发中,异常处理和日志记录是保障系统健壮性、可维护性和可观测性的关键环节。NestJS 提供了完善的异常处理体系,并支持灵活的日志集成,帮助开发者优雅地管理错误响应和系统日志。

NestJS 异常体系概览

NestJS 基于 HTTP 异常(HttpException)设计了一套统一的异常处理机制。所有未被捕获的异常,最终都会被全局异常过滤器(Exception Filter)处理,返回结构化的错误响应。

  • 内置异常类:如 BadRequestExceptionNotFoundExceptionUnauthorizedException 等,继承自 HttpException,可直接在业务代码中抛出。
  • 自定义异常:可继承 HttpException 或实现 ExceptionFilter,满足特殊业务需求。
  • 全局异常过滤器:通过 @Catch() 装饰器实现,统一处理所有未捕获的异常。
  • 拦截器:可用于统一响应格式、日志记录等。

使用内置异常类

NestJS 提供了丰富的内置异常类,推荐在业务逻辑中直接抛出,框架会自动捕获并返回标准 HTTP 响应。

import { Controller, Get, Param, NotFoundException } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('user')
export class UserController {
  constructor(private readonly userService: UserService) {}

  @Get(':id')
  async getUser(@Param('id') id: number) {
    const user = await this.userService.findById(id);
    if (!user) {
      throw new NotFoundException('用户不存在');
    }
    return user;
  }
}
  • 这样抛出的异常会自动被 NestJS 处理,返回 404 状态码和标准错误结构。

自定义异常过滤器(Exception Filter)

当需要统一错误响应格式、记录日志或处理特殊异常时,可以自定义异常过滤器。

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

@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
  private readonly logger = new Logger(AllExceptionsFilter.name);

  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() : exception;

    // 日志记录
    this.logger.error(`HTTP ${status} ${request.method} ${request.url} - ${JSON.stringify(message)}`);

    response.status(status).json({
      code: status,
      message,
      path: request.url,
      timestamp: new Date().toISOString(),
    });
  }
}
  • 通过 @Catch() 装饰器可捕获所有异常,也可指定特定异常类型。
  • 统一返回自定义的错误结构,便于前端统一处理。
  • 在过滤器中通过 Logger 记录异常日志,便于后期排查。

注册全局异常过滤器

main.ts 中注册全局异常过滤器:

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AllExceptionsFilter } from './filters/http-exception.filter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalFilters(new AllExceptionsFilter());
  await app.listen(3000);
}
bootstrap();

自定义异常类

如需自定义业务异常,可继承 HttpException,实现更细致的错误码和信息。

import { HttpException, HttpStatus } from '@nestjs/common';

export class BusinessException extends HttpException {
  constructor(message: string, code = 10001) {
    super({ code, message }, HttpStatus.BAD_REQUEST);
  }
}

// 使用
throw new BusinessException('手机号已注册', 20001);

全局拦截器统一响应格式

拦截器可用于统一 API 响应结构,无论成功还是失败都返回一致格式,便于前端处理。

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

@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  private readonly logger = new Logger(ResponseInterceptor.name);

  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const req = context.switchToHttp().getRequest();
    return next.handle().pipe(
      map(data => ({ code: 0, data, message: 'success' })),
      tap(() => {
        this.logger.log(`Response: [${req.method}] ${req.url}`);
      }),
    );
  }
}

main.ts 注册全局拦截器:

import { ResponseInterceptor } from './interceptors/response.interceptor';
app.useGlobalInterceptors(new ResponseInterceptor());

日志记录最佳实践

日志是后端系统定位问题、追踪请求、监控健康状况的重要手段。NestJS 内置了简单的 Logger,也支持集成 winston、pino 等专业日志库。

1. 日志分级与输出

  • Logger 支持 logerrorwarndebugverbose 等分级,建议根据场景选择合适的级别。
  • 生产环境建议将日志输出到文件或集中式日志平台(如 ELK、Loki、Sentry 等)。

2. 在中间件记录请求日志

可以通过中间件统一记录每个请求的访问日志:

// middlewares/logger.middleware.ts
import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  private readonly logger = new Logger('HTTP');

  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl } = req;
    res.on('finish', () => {
      const { statusCode } = res;
      this.logger.log(`${method} ${originalUrl} ${statusCode}`);
    });
    next();
  }
}

在模块中注册中间件:

// app.module.ts
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { LoggerMiddleware } from './middlewares/logger.middleware';

@Module({ /* ... */ })
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*');
  }
}

3. 日志与异常结合

  • 在异常过滤器中记录错误日志,便于追踪异常来源。
  • 在拦截器或中间件中记录请求和响应日志,便于分析接口性能和调用链。
  • 日志内容建议包含请求方法、路径、状态码、耗时、错误堆栈等关键信息。

4. 日志库扩展

  • 对于大型项目,建议集成 winston、pino 等日志库,支持日志格式化、异步写入、日志切割、远程推送等高级功能。
  • 可通过自定义 LoggerService 实现日志统一管理。

参考:NestJS 日志官方文档

实战场景:统一 API 错误返回格式与日志追踪

在实际项目中,推荐统一所有接口的错误返回结构,并结合日志记录每一次异常和请求。例如:

{
  "code": 404,
  "message": "用户不存在",
  "path": "/user/123",
  "timestamp": "2024-06-01T12:00:00.000Z"
}

这样前端可以根据 code 字段统一处理异常,后端可以通过日志快速定位问题,提升用户体验和系统可维护性。


最佳实践与常见坑点

  • 优先使用内置异常类,如 404、400、401 等,避免手写状态码。
  • 全局异常过滤器要注册在 main.ts,否则无法捕获所有异常。
  • 自定义异常要继承 HttpException,保证结构一致。
  • 拦截器和过滤器要分工明确,避免重复处理。
  • 错误信息不要暴露敏感数据,如数据库错误、堆栈信息等。
  • 日志记录建议在过滤器、中间件、拦截器中实现,便于排查问题。
  • 生产环境建议集成专业日志库,支持日志分级、持久化和远程推送。

合理的异常处理和日志记录是高质量后端的标志,建议多参考 NestJS 官方文档 和实际项目最佳实践。