首页 / NestJS 入门教程 / 中间件 Middleware

NestJS 入门教程

中间件 Middleware

本教程共 47 篇 · 第 15 篇 · 更新于 2026-08-09 · 约 9 分钟阅读

NestJS中间件MiddlewareNestMiddleware请求拦截全局中间件

本节目标:理解中间件在请求生命周期中的位置,学会写类中间件和函数中间件,掌握如何在模块中配置和排除路由。

你点了一杯奶茶,从下单到拿到手,中间经历了不少环节:接单、制作、打包、叫号。每个环节都在”最终交付”之前做一些自己的事情。

中间件就是这些环节。它在路由处理器执行之前运行,可以检查请求、修改请求、甚至直接返回响应,不让请求继续往下走。

中间件能干什么

中间件函数可以:

  • 执行任何代码
  • 修改请求和响应对象
  • 结束请求-响应周期(直接返回结果)
  • 调用 next() 把控制权交给下一个中间件

有一条铁律:如果你不调用 next(),请求就会一直挂着,客户端永远等不到响应。

Warning

忘记调用 next() 是新手最常犯的错。请求一直 pending,浏览器转圈转到天荒地老。遇到这种情况,先检查中间件里是不是漏了 next()

两种写法

类中间件

类中间件需要实现 NestMiddleware 接口,加上 @Injectable() 装饰器。好处是可以注入依赖。

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) {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
    next();
  }
}

函数中间件

如果你的中间件很简单,不需要注入任何依赖,用函数就行了。更轻量。

import { Request, Response, NextFunction } from 'express';

export function logger(req: Request, res: Response, next: NextFunction) {
  console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
  next();
}
Tip

官方建议:如果中间件不需要依赖注入,优先用函数中间件。更简洁,性能也稍好一点。

在模块中配置

中间件不是在 @Module() 装饰器里配置的。你需要让模块实现 NestModule 接口,然后在 configure() 方法中设置。

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { LoggerMiddleware } from './middleware/logger.middleware';

@Module({
  controllers: [CatsController],
})
export class CatsModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('cats');
  }
}

forRoutes('cats') 表示这个中间件只对路径以 cats 开头的路由生效。

精确控制路由

你可以更精确地指定中间件对哪个路由、哪种请求方法生效:

import { RequestMethod } from '@nestjs/common';

consumer
  .apply(LoggerMiddleware)
  .forRoutes({ path: 'cats', method: RequestMethod.GET });

这样中间件就只对 GET /cats 生效了,POST /cats 不会触发。

对控制器生效

比起写路由路径,更常见的做法是直接传控制器类。这样控制器里所有路由都会应用这个中间件:

consumer
  .apply(LoggerMiddleware)
  .forRoutes(CatsController);

排除某些路由

有时候大部分路由都需要某个中间件,但有几个例外。用 exclude() 方法:

consumer
  .apply(LoggerMiddleware)
  .exclude(
    { path: 'cats/health', method: RequestMethod.GET },
    { path: 'cats/ping', method: RequestMethod.GET },
  )
  .forRoutes(CatsController);

这样除了 healthping 这两个接口,其他所有路由都会经过 LoggerMiddleware

通配符路由

中间件也支持通配符匹配:

consumer
  .apply(LoggerMiddleware)
  .forRoutes({ path: 'cats/*splat', method: RequestMethod.ALL });

*splat 会匹配 cats/ 后面的任意字符。注意 cats/ 本身不会被匹配,如果你想连 cats/ 也匹配到,用 {*splat} 把通配符变成可选的:

consumer
  .apply(LoggerMiddleware)
  .forRoutes({ path: 'cats/{*splat}', method: RequestMethod.ALL });

多个中间件

多个中间件按顺序依次执行,用逗号隔开就行:

consumer
  .apply(LoggerMiddleware, AuthMiddleware, RateLimitMiddleware)
  .forRoutes(CatsController);

执行顺序就是 LoggerMiddleware -> AuthMiddleware -> RateLimitMiddleware -> 路由处理器。

全局中间件

如果你想让一个中间件对所有路由都生效,有两种方式。

方式一:在 main.ts 中用 app.use()

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './middleware/logger.middleware';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.use(logger);
  await app.listen(3000);
}
bootstrap();

这种方式简单直接,但没法注入依赖。

方式二:在模块中用 forRoutes('*')

@Module({
  imports: [CatsModule],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer
      .apply(LoggerMiddleware)
      .forRoutes('*');
  }
}

这种方式支持依赖注入,推荐用这种。

Note

forRoutes('*') 里的 * 是通配符,匹配所有路由。如果你用的是函数中间件,直接在 main.tsapp.use() 更方便。

中间件执行顺序

理解中间件的执行顺序很重要。一个请求从进来到出去,经过的环节是这样的:

请求进入
  → 全局中间件
    → 模块中间件
      → 路由处理器(Controller)
        → Service
      ← 响应返回


← 响应发送给客户端

全局中间件最先执行,模块中间件其次,然后才到控制器里的路由处理器。

Tip

中间件是在控制器之前执行的。所以如果你需要在路由处理之前做一些通用操作(比如记日志、检查 token),中间件是合适的选择。

实用中间件示例

日志中间件

记录每个请求的方法、路径、状态码和耗时:

import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

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

  use(req: Request, res: Response, next: NextFunction) {
    const { method, originalUrl, ip } = req;
    const startTime = Date.now();

    // 监听响应完成事件,拿到状态码和耗时
    res.on('finish', () => {
      const { statusCode } = res;
      const duration = Date.now() - startTime;
      this.logger.log(
        `${method} ${originalUrl} ${statusCode} ${duration}ms - ${ip}`,
      );
    });

    next();
  }
}

这个中间件的巧妙之处在于:调用 next() 之后请求继续往下走,但 res.on('finish') 的回调会在响应发送完毕后执行,这时候就能拿到最终的状态码和总耗时了。

认证中间件

检查请求头里的 token,没有就拒绝:

import {
  Injectable,
  NestMiddleware,
  UnauthorizedException,
} from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AuthMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const token = req.headers.authorization?.split(' ')[1];

    if (!token) {
      throw new UnauthorizedException('缺少认证 token');
    }

    try {
      // 验证 token 的逻辑
      const decoded = this.verifyToken(token);
      // 把用户信息挂到 request 上,后续路由处理器可以直接用
      (req as any).user = decoded;
      next();
    } catch {
      throw new UnauthorizedException('token 无效或已过期');
    }
  }

  private verifyToken(token: string): any {
    // 实际的 token 验证逻辑
    return { id: 1, name: 'test' };
  }
}
Note

在中间件中抛出的异常会被 NestJS 的异常处理层捕获,走异常过滤器的逻辑。所以你可以放心地在中间件里用 throw 抛异常。

请求 ID 中间件

给每个请求分配一个唯一 ID,方便日志追踪:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';

@Injectable()
export class RequestIdMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    const requestId = req.headers['x-request-id'] as string || randomUUID();
    req.headers['x-request-id'] = requestId;
    res.setHeader('X-Request-Id', requestId);
    next();
  }
}

如果客户端传了 X-Request-Id,就用客户端的;没有就自动生成一个。这样在整个请求链路中,所有日志都可以带上这个 ID,排查问题时一目了然。

中间件里注入依赖

类中间件支持依赖注入,可以注入同模块内的 Provider:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class ApiKeyMiddleware implements NestMiddleware {
  constructor(private configService: ConfigService) {}

  use(req: Request, res: Response, next: NextFunction) {
    const apiKey = this.configService.get<string>('API_KEY');
    if (apiKey) {
      req.headers['x-api-key'] = apiKey;
    }
    next();
  }
}
Tip

函数中间件没法注入依赖。如果你需要用到其他服务,必须用类中间件。

异步中间件

中间件的 use 方法可以是异步的:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class AsyncMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    // 做一些异步操作,比如查数据库、调外部 API
    const result = await this.checkSomething(req);
    
    if (!result) {
      throw new ForbiddenException('不允许访问');
    }
    
    next();
  }

  private async checkSomething(req: Request): Promise<boolean> {
    // 模拟异步操作
    return true;
  }
}
Warning

异步中间件里如果出了异常,记得要么 throw 出去,要么 next(error)。不要让异常默默吞掉,否则请求会一直挂起。

错误处理

中间件里如果发生了同步错误,可以用 try/catch 包裹 next()

@Injectable()
export class ErrorHandlingMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    try {
      // 一些可能出错的逻辑
      this.doSomethingRisky(req);
      next();
    } catch (error) {
      // 把错误传给下一个错误处理器
      next(error);
    }
  }

  private doSomethingRisky(req: Request) {
    // ...
  }
}

调用 next(error) 会把错误传递给 NestJS 的异常处理层,最终由异常过滤器处理。

中间件 vs 守卫 vs 拦截器

NestJS 有好几种”拦截”请求的机制,容易搞混。简单区分一下:

机制执行时机典型用途
中间件路由处理器之前日志、CORS、请求修改
守卫路由处理器之前(中间件之后)认证、授权
拦截器路由处理器前后响应转换、缓存

中间件更适合做”通用”的请求处理,跟业务逻辑无关的那种。如果你要做权限校验,用守卫更合适。

Note

NestJS 官方推荐:能用守卫解决的,不要用中间件。中间件更适合那些跟 Express/Fastify 层面相关的操作。

本章小结

这一章讲了 NestJS 中间件的核心知识:

  • 中间件在路由处理器之前执行,可以修改请求、响应,或者中断请求
  • 两种写法:类中间件(支持依赖注入)和函数中间件(更轻量)
  • 在模块的 configure() 方法中配置,用 MiddlewareConsumer 链式调用
  • forRoutes() 支持路由路径、控制器类、通配符等多种匹配方式
  • exclude() 可以排除不需要中间件的路由
  • 全局中间件可以用 app.use()forRoutes('*')
  • 中间件执行顺序:全局 -> 模块 -> 路由处理器

中间件是请求处理链路中最早的一环。下一章我们聊管道,看看怎么在参数到达控制器之前做验证和转换。