中间件 Middleware
本教程共 47 篇 · 第 15 篇 · 更新于 2026-08-09 · 约 9 分钟阅读
本节目标:理解中间件在请求生命周期中的位置,学会写类中间件和函数中间件,掌握如何在模块中配置和排除路由。
你点了一杯奶茶,从下单到拿到手,中间经历了不少环节:接单、制作、打包、叫号。每个环节都在”最终交付”之前做一些自己的事情。
中间件就是这些环节。它在路由处理器执行之前运行,可以检查请求、修改请求、甚至直接返回响应,不让请求继续往下走。
中间件能干什么
中间件函数可以:
- 执行任何代码
- 修改请求和响应对象
- 结束请求-响应周期(直接返回结果)
- 调用
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);
这样除了 health 和 ping 这两个接口,其他所有路由都会经过 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.ts里app.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、请求修改 |
| 守卫 | 路由处理器之前(中间件之后) | 认证、授权 |
| 拦截器 | 路由处理器前后 | 响应转换、缓存 |
中间件更适合做”通用”的请求处理,跟业务逻辑无关的那种。如果你要做权限校验,用守卫更合适。
NoteNestJS 官方推荐:能用守卫解决的,不要用中间件。中间件更适合那些跟 Express/Fastify 层面相关的操作。
本章小结
这一章讲了 NestJS 中间件的核心知识:
- 中间件在路由处理器之前执行,可以修改请求、响应,或者中断请求
- 两种写法:类中间件(支持依赖注入)和函数中间件(更轻量)
- 在模块的
configure()方法中配置,用MiddlewareConsumer链式调用 forRoutes()支持路由路径、控制器类、通配符等多种匹配方式exclude()可以排除不需要中间件的路由- 全局中间件可以用
app.use()或forRoutes('*') - 中间件执行顺序:全局 -> 模块 -> 路由处理器
中间件是请求处理链路中最早的一环。下一章我们聊管道,看看怎么在参数到达控制器之前做验证和转换。