这篇文章写给:刚开始看 NestJS,看到一堆名词(Module / Provider / Guard / Pipe…)有点懵的人。
目标也很朴素:看完之后,你至少能回答两件事:“它们分别负责啥?”、“我该把代码放哪?”
我刚接触 NestJS 的时候,最大的困惑其实不是语法,而是:同样是写接口,为什么多了这么多“角色”?
这篇就专门把这些角色捋顺。
结论先行(先背这一段就够了)
NestJS 里常见的“基础元素”大概就 8 类:Module、Controller、Provider、Middleware、Guard、Pipe、Interceptor、Exception Filter。
如果你只想先跑起来、先做业务,记住这个版本就行:
- 三件套(业务主干):
Controller负责接请求,Provider负责干活,Module负责把它们组织起来 - 五件套(横切能力):
Middleware / Guard / Pipe / Interceptor / Exception Filter分别解决:请求前处理 / 访问控制 / 入参校验与转换 / 前后包装 / 统一错误输出
最小可用应用通常只需要 Module + Controller + Provider,剩下 5 个都是“你遇到问题了再上”的工具箱。
最小骨架(伪代码,够你对上结构)
// main.ts
const app = await NestFactory.create(AppModule);
await app.listen(3000);
// app.module.ts
@Module({
controllers: [UserController],
providers: [UserService],
})
export class AppModule {}
一句话:Controller 只写路由与入参,Service/Provider 只写业务逻辑与数据访问。你先按这个习惯写,后面再谈扩展。
下面我会先用 1 分钟聊聊 NestJS 和其他框架的差异,然后按元素逐个拆开讲。
Nest 与 Egg、Express、Koa 的区别
| 框架 | 定位 | 特点 |
|---|---|---|
| Express | 极简 HTTP 库 | 无约定、无结构,自由度高,适合小项目或需要完全自定义的场景 |
| Koa | 更轻的 HTTP 框架内核 | 洋葱模型中间件,更依赖中间件生态与工程约定,仍无内置架构 |
| Egg | 企业级 Node 框架 | 约定优于配置,内置多进程、插件体系,偏阿里生态 |
| NestJS | 企业级 Node 框架 | 借鉴 Angular,依赖注入 + 装饰器 + 模块化,TypeScript 优先 |
你可以把它们理解成“自由度 vs 约束”的不同取舍:
- Express/Koa:你想怎么搭都行(爽),但团队一大就容易“各写各的”(痛)
- Egg:约定更强,工程化能力更完整,但它的那套抽象和生态是另一条路线
- NestJS:更像“带架子的 Express”(默认底层就是 Express,也能换 Fastify),通过 模块边界 + DI(依赖注入)+ 装饰器,把“组织代码这件事”做得更系统
所以 为何 NestJS 受欢迎:项目越大、协作越多人,越需要统一的分层与边界;NestJS 在这方面帮你省心。
但也别只看好处:代价就是学习曲线更陡、抽象更多、样板代码也更多;如果只是 1-2 个接口的小服务,Express/Koa 往往更轻。
如果你现在纠结选型,我比较粗暴的经验是(不绝对):
- Express/Koa:短平快、强自定义,团队能自己定规范并坚持执行
- Egg:团队熟悉其目录约定与插件体系,并且更吃它的工程化生态
- NestJS:项目会长期演进、模块多、协作多,希望用 DI/模块化把复杂度“关在笼子里”
模块 Module
先说人话:Module 就是“一个功能域的打包盒子”。它把 Controller、Provider 以及依赖关系“声明出来”,让 NestJS 能构建依赖图。
在其他框架里怎么对号入座?
- Express/Koa:更多是靠目录划分 + 手动
import(模块边界是“约定出来的”) - Egg:靠目录约定与插件机制组织,不强调
@Module()这种显式边界
@Module({
imports: [DatabaseModule], // 依赖的模块
controllers: [UserController],
providers: [UserService],
exports: [UserService], // 供其他模块使用
})
export class UserModule {}
什么时候你会明显觉得它有用:模块一多、团队一多,你不想全项目到处 import 来 import 去;你希望“我只需要 import 一个模块,就能拿到它对外提供的能力”。
控制器 Controller
一句话:Controller 负责“把 HTTP 请求接住”,不要在这里堆业务细节。
在其他框架里,它最像:
- Express:
app.get('/users/:id', handler)/express.Router()的路由处理函数 - Koa:配合
koa-router的router.get()/router.post()处理函数 - Egg:
app/controller/*.js里的 Controller 类方法
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
@Post()
create(@Body() dto: CreateUserDto) {
return this.userService.create(dto);
}
}
什么时候用:只要你要对外提供 HTTP 接口,就会写 Controller。
一个小习惯:Controller 里多写“参数提取/校验/调用 service”,少写“业务规则/SQL/第三方调用”。
提供者 Provider
一句话:Provider 就是干活的人(Service、Repository、各种工具类都算)。它的核心价值是:NestJS 会帮你把依赖“注入”进来,你不用满世界手动 new。
对应到其他框架:
- Express/Koa:你也会写
service/repository,但大多是手动import、手动组织依赖 - Egg:最接近的是
app/service/*.js(通过ctx.service.xxx使用)
一个非常常见的坑:Provider 默认是单例(同一个实例服务所有请求)。所以别把“当前登录用户/本次请求的临时变量”塞到成员变量里,不然就会出现串请求的灵异问题。一般放在方法参数里就好;确实需要再考虑 request-scoped。 (这个坑我见过也踩过一次,排查起来很费劲……)
@Injectable()
export class UserService {
constructor(private db: DatabaseService) {}
findOne(id: string) {
return this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
}
什么时候用:只要有可复用的业务逻辑、数据访问、外部系统交互(DB / API / MQ),就别犹豫,放 Provider 里。
中间件 Middleware
一句话:Middleware 就是“请求刚进门时的门卫”,在路由之前跑。
这一块和 Express/Koa/Egg 的 middleware 很像;区别更多在“你把哪些事情放在 middleware”——NestJS 里它更偏 HTTP 层入口前处理。
export class LoggerMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`${req.method} ${req.url}`);
next();
}
}
// 在模块中: configure(consumer) { consumer.apply(LoggerMiddleware).forRoutes('*'); }
什么时候用:日志、CORS、限流、一些很通用的请求预处理(越靠近 HTTP 越适合放这)。
守卫 Guard
一句话:Guard 专门管“能不能进”(鉴权/权限),比把所有东西都塞进 middleware 更清晰。
在 Express/Koa/Egg 里,它通常就是“鉴权中间件/权限中间件”;NestJS 只是把它单独命名出来,让职责更聚焦。
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
return !!request.headers['authorization'];
}
}
// 使用: @UseGuards(AuthGuard) 装饰在控制器或方法上
什么时候用:鉴权,以及鉴权后的权限校验(角色/资源归属/是否允许操作)。
管道 Pipe
一句话:Pipe 管“入参是不是靠谱”,顺便做点类型转换。
在 Express/Koa/Egg 里你一般会在 handler 或中间件里做校验;NestJS 把它抽成 Pipe,常和 ValidationPipe/DTO 搭配,目的是把“校验”从业务代码里移出去。
@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
transform(value: string) {
const val = parseInt(value, 10);
if (isNaN(val)) throw new BadRequestException('无效的 ID');
return val;
}
}
// 使用: findOne(@Param('id', ParseIntPipe) id: number)
什么时候用:参数类型转换、DTO 校验(配合 class-validator)、默认值处理。实际项目里最常用的是内置的 ValidationPipe。
拦截器 Interceptor
一句话:Interceptor 像“可前可后”的中间件,但更贴近 Controller 的执行与返回值。
你会在这里做:统一响应结构、耗时统计、缓存、超时控制这类“包一层”的事情。
@Injectable()
export class TransformInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler) {
return next.handle().pipe(
map(data => ({ code: 0, data })),
);
}
}
什么时候用:统一响应结构、日志/埋点、缓存、超时控制。它比 middleware 更贴近业务层(能拿到路由上下文和返回值)。
异常过滤器 Exception Filter
一句话:Exception Filter 负责把错误“翻译”成你想要的 HTTP 输出(状态码、错误体格式)。
类比一下:
- Express:错误处理中间件
(err, req, res, next) - Koa:常见写法是错误中间件 +
app.on('error') - Egg:
onerror插件/错误处理中间件
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException, host: ArgumentsHost) {
const ctx = host.switchToHttp();
const response = ctx.getResponse();
response.status(exception.getStatus()).json({
statusCode: exception.getStatus(),
message: exception.message,
});
}
}
什么时候用:你想统一错误格式、区分生产/开发环境返回内容、或者对接前端错误约定的时候。
请求生命周期(执行顺序)
中间件 → 守卫 → 拦截器(before) → 管道 → 控制器 → 拦截器(after) → 异常过滤器(若抛错)
理解这个顺序有助于判断:某逻辑该放在 Middleware、Guard 还是 Interceptor。
Nest 元素与框架对应关系
| Nest 元素 | Express | Koa | Egg |
|---|---|---|---|
| Module | 无内置,按目录/文件组织 | 无内置 | 无严格 Module,按目录约定(如 app/controller) |
| Controller | app.get/post() + 路由处理函数,或 express.Router() | 常配合 koa-router 的 router.get/post() | Controller 类 + 方法(async index()) |
| Provider | 普通函数/类,手动 require 或 import 后调用 | 同上 | Service 类(ctx.service.xxx) |
| Middleware | app.use(fn),(req, res, next) => {} | app.use(fn),(ctx, next) => {} | config.middleware 配置 + app.use |
| Guard | 鉴权中间件,next() 放行或 res.status(403).end() | 鉴权中间件,next() 或 ctx.throw(403) | 中间件或 Service 内手动校验 |
| Pipe | 路由处理函数内手动校验/转换,或中间件 | 同上 | Controller/Service 内手动校验 |
| Interceptor | 包装响应的中间件(res.json 前处理) | 中间件(ctx.body 赋值前后) | 中间件 |
| Exception Filter | 错误处理中间件 (err, req, res, next) | app.on('error') 或错误中间件 | onerror 插件、app.on('error') |
简要说明:
- Express/Koa 无 Module、Provider、Guard、Pipe 等概念,对应能力需自行用中间件或业务代码实现。
- Egg 有 Controller、Service,与 Nest 较接近,但无装饰器、无 DI,依赖通过
ctx传递。
小结
| 元素 | 职责 | 典型场景 |
|---|---|---|
| Module | 组织与边界 | 按业务域拆分 |
| Controller | 路由与请求入口 | 定义 API |
| Provider | 业务逻辑与依赖 | Service、Repository |
| Middleware | 请求前处理 | 日志、CORS、限流 |
| Guard | 访问控制 | 鉴权、权限 |
| Pipe | 参数校验与转换 | DTO 校验、类型转换 |
| Interceptor | 请求-响应包装 | 统一响应、缓存 |
| Exception Filter | 异常转 HTTP | 统一错误格式 |
最小可用 Nest 应用只需:Module + Controller + Provider。其余元素按需引入,避免过度设计。
如果你现在回头看那 8 个名词,应该至少能把它们放进“请求链路”里:请求先进 Middleware,再经过 Guard/Pipe/Interceptor,最后到 Controller 调 Provider 干活;出错就交给 Exception Filter。能对上这张图,后面再看 NestJS 的文档/源码就顺很多了。
最后附上
以上是我对 NestJS 的一些整理与理解,欢迎在评论区补充/讨论;如果哪里有偏差,也欢迎直接指出,我会及时修正。