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

12 阅读9分钟

这篇文章写给:刚开始看 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 {}

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


控制器 Controller

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

在其他框架里,它最像:

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