NestJS 中间件
目录
中间件
中间件是在路由处理程序之前执行的函数,主要用于拦截请求、记录日志、权限校验、修改请求对象等,必须调用 next()
继续执行链条。
中间件函数可以访问 request
和 response
对象,以及应用请求-响应周期中的 next()
函数。
默认情况下,Nest 中间件等同于 Express 中间件。
(req, res, next) => { /* 逻辑 */ }
官方 Express 文档说明,中间件可以:执行任意代码,修改 req/res,结束请求,或调用 next()
继续往下走,如果你不调用 next()
,请求就“卡死”了,浏览器会一直等待响应。
函数式中间件
function logger(req, res, next) {
console.log('Request...');
next();
}
类中间件:
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(`Incoming Request to: ${req.method} ${req.url}`);
next(); // 放行到下一个中间件或控制器
}
}
使用 @Injectable()
声明,需要实现 NestMiddleware
接口,use()
方法中定义逻辑,支持依赖注入(比如你可以注入日志服务)。
需要注意Express 和 Fastify 的差异,如果切换到 Fastify,不要盲目照搬 Express 的中间件
依赖注入
Nest 中间件完全支持依赖注入,就像 provider 和 controller 一样,它们可以注入同一模块中注册(可用)的依赖,和之前一样,这通过 constructor
实现。
应用中间件
Nest 中间件的注册不是写在 @Module()
装饰器里,而是通过模块类的 configure()
方法,在其中使用 MiddlewareConsumer
显式注册。
@Module()
装饰器中没有中间件的位置,我们使用模块类的 configure()
方法注册中间件。
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}
apply()
:传入中间件(可以多个)
forRoutes()
:指定哪些路由使用中间件(支持字符串、对象、控制器类)
NestModule
:必须实现这个接口才能使用 configure()
方法
支持更精准地匹配路由和方法
import { Module, NestModule, RequestMethod, MiddlewareConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middleware/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({ path: 'cats', method: RequestMethod.GET });
}
}
只作用于 GET /cats
请求,可配合 RequestMethod.POST / PUT / DELETE
精确控制
forRoutes(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST }
);
你也可以作用于整个控制器类:
forRoutes(CatsController)
配置方式支持 async,configure()
方法可以是异步的。
async configure(consumer: MiddlewareConsumer) {
const config = await this.configService.load();
consumer.apply(MyMiddleware).forRoutes(...);
}
虽然不常见,但在某些需要“等待配置完成”后再决定加载哪些中间件的场景中是有用的。
注意:如果你使用的是 Express(默认适配器),Nest 会自动注册 body-parser
。
这意味着:
- Nest 启动时已经注入了
express.json()
和express.urlencoded()
; - 如果你想用自己的 JSON 解析器或参数处理中间件,比如:
app.use(bodyParser.json({ limit: '10mb' }));
你必须在创建应用时关闭 Nest 默认注入的 bodyParser:
const app = await NestFactory.create(AppModule, {
bodyParser: false,
});
推荐中间件注册结构:
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware, OtherMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.POST }, // 不作用于 POST /cats
)
.forRoutes(CatsController); // 对 CatsController 所有方法生效
}
}
路由通配符
Nest 支持使用路径通配符(如 *splat
)匹配特定模式的路由,非常适合中间件批量作用于一类路径,比如静态资源、API 前缀、带动态参数的路由等。
forRoutes({
path: 'abcd/*splat',
method: RequestMethod.ALL,
});
上面这个配置会匹配:
/abcd/1
/abcd/abc
/abcd/anything/here
splat
只是通配符的“名字”,没有任何语义限制,你可以叫它 *wildcard
、*pathTail
,任意名称都可以。
注意:path: 'abcd/*'
不会匹配 /abcd/
本身,不会匹配 /abcd/
,因为没有“*”后面的部分。
匹配带或不带尾部的路径:使用 {*splat}
path: 'abcd/{*splat}'
这会匹配:
/abcd/
/abcd/x
/abcd/x/y
花括号 {}
的意思是:整个通配符段是可选的。
举个实战应用:
consumer
.apply(AuthMiddleware)
.forRoutes({
path: 'api/{*wildcard}',
method: RequestMethod.ALL,
});
上面代码会让中间件作用于所有以 /api/
开头的路由,包括 /api
, /api/user
, /api/user/123/edit
。
建议:
不推荐用太宽泛的通配符(如 *
),否则中间件会作用于所有请求,可能带来性能问题。
path: '{*splat}'
适合用在全局 fallback 中间件,比如前端 SPA 支持(比如所有非 API 路由跳转到 index.html
)。
中间件消费者
MiddlewareConsumer
是一个中间件管理器,提供链式方法如 .apply()
、.forRoutes()
、.exclude()
等,用于灵活精确地控制中间件在哪些路由上生效。
forRoutes()
接收的参数类型很多样:
类型 | 示例 | 含义 |
---|---|---|
字符串 | 'cats' | 匹配 /cats 路由 |
多个字符串 | 'cats', 'dogs' | 匹配多个路径 |
控制器类 | CatsController | 匹配此控制器中所有路由 |
多个控制器类 | CatsController, DogsController | 多控制器批量注册 |
RouteInfo 对象 | { path: 'cats', method: RequestMethod.GET } | 精确指定路径 + 请求方法(支持多个对象) |
示例:作用于单个控制器
@Module({
imports: [CatsModule],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes(CatsController);
}
}
这表示:LoggerMiddleware
会作用于 CatsController
的所有方法和路径。
提示:apply() 支持多个中间件
consumer
.apply(LoggerMiddleware, AuthMiddleware, CompressionMiddleware)
.forRoutes(CatsController);
你可以一次性注册多个中间件,这些中间件会按顺序依次执行。
用法 | 建议 |
---|---|
apply(...).forRoutes('prefix') | 匹配精确的 /prefix 路径 |
apply(...).forRoutes(SomeController) | 用于作用于整个控制器 |
apply(...).forRoutes({ path: ..., method: ... }) | 精细控制请求路径和方法 |
apply(...).exclude(...) | 配合 exclude() 排除特定路由(比如某些无需认证的路径) |
.exclude()
示例(经典登录绕过场景):
consumer
.apply(AuthMiddleware)
.exclude({ path: 'auth/login', method: RequestMethod.POST })
.forRoutes('*');
这会让中间件作用于所有路由,但排除掉 POST /auth/login
。
排除路由
.exclude()
方法可以排除特定路由,使中间件不作用于这些路径,适用于登录白名单、Webhook绕过、安全路由控制等场景。
exclude()
方法接受单个字符串、多个字符串或 RouteInfo
对象来标识要排除的路由,.exclude()
和 .forRoutes()
的参数类型 基本一致
比如你想中间件作用于整个控制器,但排除其中一两个方法(如 /cats
的 GET、POST)。
consumer
.apply(LoggerMiddleware)
.exclude(
{ path: 'cats', method: RequestMethod.GET },
{ path: 'cats', method: RequestMethod.POST },
'cats/{*splat}',
)
.forRoutes(CatsController);
将 LoggerMiddleware
应用于 CatsController
的所有路由,但排除以下路由:
GET /cats
POST /cats
- 所有
/cats/xxx
子路径
exclude()
是对 .forRoutes()
的补充,它是优先生效的:
先执行 exclude()
过滤掉你不希望中间件生效的路径,剩下的交给 forRoutes()
匹配范围
路径匹配使用的是 path-to-regexp
包(Express 也是这个),所以你可以放心使用通配符:*
、{*splat}
、参数等
功能中间件
如果你的中间件只是做些简单的请求日志、路径检查、IP 过滤等逻辑,没有依赖注入或状态管理,就可以用一个简单的函数来定义,而不需要写一个类。
👎 基于类的中间件(麻烦):
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('Request...');
next();
}
}
👍 函数式中间件(推荐用于轻量场景):
import { Request, Response, NextFunction } from 'express';
export function logger(req: Request, res: Response, next: NextFunction) {
console.log('Request...');
next();
}
注册方式一样:
consumer
.apply(logger)
.forRoutes(CatsController);
函数式中间件的适用场景:不需要依赖注入,只打印日志、设置 headers、IP 白名单等,没有内部状态,纯逻辑
不建议使用函数式的情况:中间件需要注入服务,需要配置项(如读取 configService), 需要访问模块上下文,但只有类支持依赖注入 & 生命周期
优雅使用:
- 放在
common/middleware/logger.middleware.ts
- 对于中间件名,可以规范命名为
loggerMiddleware()
或logger
,函数名小写。 - 有状态的中间件 → 用类;无状态 → 用函数。
建议:当你的中间件不需要任何依赖时,请考虑使用更简单的功能中间件替代方案。
多个中间件
为了绑定顺序执行的多个中间件,只需在 apply()
方法中提供一个逗号分隔的列表:
consumer.apply(cors(), helmet(), logger).forRoutes(CatsController);
它们的执行顺序是:cors()
-> helmet()
-> logger()
,即从左到右依次执行。
类中间件写类名:LoggerMiddleware
函数式中间件写函数:logger
(不加括号)
第三方中间件(如 cors
)写函数调用结果:cors()
Nest完全兼容 Express 和 Fastify 的中间件生态,你可以直接使用大量成熟的中间件库
常用第三方中间件库
适用于 Express 适配器:
中间件 | 说明 | 安装 |
---|---|---|
helmet | 设置各种安全 HTTP 头部,防止 XSS、点击劫持等攻击 | pnpm add helmet |
cors | 开启跨域资源共享 | pnpm add cors |
morgan | 请求日志记录器,支持不同格式(如 combined 、dev ) | pnpm add morgan |
compression | Gzip 压缩 HTTP 响应体,提升传输效率 | pnpm add compression |
express-rate-limit | 限流中间件,防止接口刷爆 | pnpm add express-rate-limit |
express-session | 处理 Session 会话管理(配合身份认证) | pnpm add express-session |
cookie-parser | 解析 HTTP 请求中的 Cookie | pnpm add cookie-parser |
express-useragent | 解析请求头中的 user-agent 字符串 | pnpm add express-useragent |
在 NestJS 中使用
import * as helmet from 'helmet';
import * as cors from 'cors';
import * as morgan from 'morgan';
import * as compression from 'compression';
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(
helmet(),
cors(),
compression(),
morgan('dev')
)
.forRoutes('*');
}
}
如果你切换到 Fastify(@nestjs/platform-fastify
),则要使用 Fastify 对应的插件:
插件 | 替代 Express 中间件 |
---|---|
@fastify/cors | 替代 cors |
@fastify/helmet | 替代 helmet |
@fastify/compress | 替代 compression |
@fastify/rate-limit | 替代 express-rate-limit |
通过 app.register(...)
注册插件,而不是 consumer.apply(...)
建议组合(开发 + 生产)
用途 | 推荐中间件 |
---|---|
安全 | helmet() 、rate-limit() |
性能 | compression() 、Fastify 也推荐 |
监控 | morgan('dev') + 自定义 logger |
用户端信息 | express-useragent |
身份认证 | cookie-parser() + session() (配合 Auth) |
全局中间件
如果我们想一次将中间件绑定到每个已注册的路由,我们可以使用 INestApplication
实例提供的 use()
方法
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { logger } from './common/middleware/logger.middleware';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 绑定全局中间件(函数式)
app.use(logger);
await app.listen(process.env.PORT ?? 3000);
}
bootstrap();
特点说明
特性 | 说明 |
---|---|
作用范围 | 绑定到 所有 路由 |
使用方式 | app.use(...) |
推荐中间件类型 | 函数式中间件(推荐),也支持类(但不能注入依赖) |
注册位置 | 必须在 main.ts 中使用 INestApplication 实例 |
调用时机 | 在所有模块加载完后运行 |
注意:全局中间件不能使用依赖注入
类中间件如果通过 app.use(...)
注册,是无法注入依赖的!因为它脱离了 Nest 的模块体系。
@Injectable()
export class LoggerMiddleware implements NestMiddleware {
constructor(private configService: ConfigService) {} // 不会注入
}
所以,用函数式中间件(比如纯 logger 函数)放在全局就好, 需要依赖注入的类中间件不要用 app.use()
注册。
支持依赖注入的“伪全局中间件”
如果你想要:中间件作用于整个 app(几乎所有路由),又希望能注入依赖(比如 ConfigService
、LoggerService
)
consumer
.apply(LoggerMiddleware)
.forRoutes('*'); // 匹配所有路由,包括 Controller 中的所有 handler
这种方式中间件能像正常提供器一样注入依赖,还能通过 exclude()
精细控制范围。