构建一个 NestJS 应用程序需要具备哪些基础元素?

这篇文章写给:刚开始看 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 个都是"你遇到问题了再上"的工具箱。

最小骨架(伪代码,够你对上结构)

typescript 复制代码
// main.ts
const app = await NestFactory.create(AppModule);
await app.listen(3000);
typescript 复制代码
// 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() 这种显式边界
typescript 复制代码
@Module({
  imports: [DatabaseModule],      // 依赖的模块
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],         // 供其他模块使用
})
export class UserModule {}

什么时候你会明显觉得它有用 :模块一多、团队一多,你不想全项目到处 importimport 去;你希望"我只需要 import 一个模块,就能拿到它对外提供的能力"。


控制器 Controller

一句话:Controller 负责"把 HTTP 请求接住",不要在这里堆业务细节

在其他框架里,它最像:

  • Express:app.get('/users/:id', handler) / express.Router() 的路由处理函数
  • Koa:配合 koa-routerrouter.get() / router.post() 处理函数
  • Egg:app/controller/*.js 里的 Controller 类方法
typescript 复制代码
@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。 (这个坑我见过也踩过一次,排查起来很费劲......)

typescript 复制代码
@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 层入口前处理。

typescript 复制代码
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 只是把它单独命名出来,让职责更聚焦。

typescript 复制代码
@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 搭配,目的是把"校验"从业务代码里移出去。

typescript 复制代码
@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 的执行与返回值。

你会在这里做:统一响应结构、耗时统计、缓存、超时控制这类"包一层"的事情。

typescript 复制代码
@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 插件/错误处理中间件
typescript 复制代码
@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,
    });
  }
}

什么时候用:你想统一错误格式、区分生产/开发环境返回内容、或者对接前端错误约定的时候。


请求生命周期(执行顺序)

scss 复制代码
中间件 → 守卫 → 拦截器(before) → 管道 → 控制器 → 拦截器(after) → 异常过滤器(若抛错)

理解这个顺序有助于判断:某逻辑该放在 Middleware、Guard 还是 Interceptor。


Nest 元素与框架对应关系

Nest 元素 Express Koa Egg
Module 无内置,按目录/文件组织 无内置 无严格 Module,按目录约定(如 app/controller
Controller app.get/post() + 路由处理函数,或 express.Router() 常配合 koa-routerrouter.get/post() Controller 类 + 方法(async index()
Provider 普通函数/类,手动 requireimport 后调用 同上 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 的一些整理与理解,欢迎在评论区补充/讨论;如果哪里有偏差,也欢迎直接指出,我会及时修正。

相关推荐
UIUV4 小时前
AI Agent 开发实战:从原理到最小化实现
后端·langchain·node.js
2301_816997884 小时前
Webpack基础
前端·webpack·node.js
Qinana5 小时前
解构 LangChain Tool Calling:从 Schema 定义到 Agent 执行循环的深度解析
前端·javascript·node.js
朝朝暮暮an6 小时前
Day 4|Node.js 文件系统、路径与进程管理
node.js
朝朝暮暮an6 小时前
Day 3|Node.js 事件循环 & 异步模型深入(Part 2,3)
node.js
章丸丸1 天前
Tube - Video Reactions
react.js·node.js·next.js
折七1 天前
2026 年 Node.js 后端技术选型,为什么我选了 Hono 而不是 NestJS
前端·后端·node.js
我叫唧唧波1 天前
【NodeJS】从入门到进阶
node.js
张3蜂1 天前
Node.js 安装与配置完全指南:从零开始搭建开发环境
node.js