NestJS控制器和路由:装饰器、参数获取、响应处理和常见坑
NestJS 的控制器用装饰器声明路由,不用手动写 app.get('/users/:id', ...)——装饰器既是文档又是路由注册。这篇文章把控制器的声明、路由参数获取、响应处理、以及常见的坑都过一遍。
基本路由声明
typescript@Controller('users') // 路由前缀 /users export class UsersController { constructor(private readonly usersService: UsersService) {} @Get() // GET /users findAll() { return this.usersService.findAll(); } @Get(':id') // GET /users/:id findOne(@Param('id') id: string) { return this.usersService.findOne(id); } @Post() // POST /users create(@Body() dto: CreateUserDto) { return this.usersService.create(dto); } @Put(':id') // PUT /users/:id update(@Param('id') id: string, @Body() dto: UpdateUserDto) { return this.usersService.update(id, dto); } @Delete(':id') // DELETE /users/:id remove(@Param('id') id: string) { return this.usersService.remove(id); } }
NestJS 自动把返回值序列化为 JSON,状态码默认 200(POST 是 201)。不需要手动 res.json()。
路由参数的获取方式
| 装饰器 | 来源 | 示例 |
|---|---|---|
@Param('id') | 路径参数 | /users/42 → "42" |
@Query('page') | 查询参数 | ?page=2 → "2" |
@Body() | 请求体 | {"name": "Tom"} → { name: "Tom" } |
@Headers('auth') | 请求头 | Authorization: Bearer ... |
@Ip() | 客户端 IP | |
@Session() | Express session |
路径参数
typescript@Get(':id') findOne(@Param('id') id: string) { // 拿单个参数 return this.usersService.findOne(id); } @Get(':category/:id') findByCategory( @Param('category') category: string, @Param('id') id: string, // 多个路径参数 ) {}
查询参数
typescript@Get() findAll( @Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number, @Query('limit', new DefaultValuePipe(20), ParseIntPipe) limit: number, @Query('sort') sort?: string, ) { return this.usersService.findAll({ page, limit, sort }); }
多个管道按参数位置从左到右执行:DefaultValuePipe 先设默认值,ParseIntPipe 再转数字。
请求体 + DTO
typescript@Post() create(@Body() dto: CreateUserDto) { return this.usersService.create(dto); }
DTO 配合 ValidationPipe 使用,验证逻辑在 DTO 类上声明,控制器保持干净。
响应处理
修改状态码
typescript@Post() @HttpCode(200) // POST 默认 201,改成 200 create(@Body() dto: CreateUserDto) {} @Delete(':id') @HttpCode(204) // 删除成功返回 204 No Content remove(@Param('id') id: string) {}
设置响应头
typescript@Get() @Header('Cache-Control', 'max-age=3600') findAll() {}
重定向
typescript@Get('docs') @Redirect('https://docs.nestjs.com', 302) redirectToDocs() {}
动态重定向(返回值覆盖装饰器):
typescript@Get('docs') @Redirect('https://docs.nestjs.com') redirectToDocs(@Query('version') version?: string) { if (version === 'v7') { return { url: 'https://v7.docs.nestjs.com' }; } }
流式响应
大文件下载、SSE 等场景需要流式返回:
typescriptimport { Observable } from 'rxjs'; @Get('stream') streamData(): Observable<MessageEvent> { return interval(1000).pipe( map(() => ({ data: `Time: ${new Date().toISOString()}` })), ); }
返回 Observable 或 Stream 时,NestJS 自动处理背压和清理。
手动操作 Response
需要完全控制响应时(如设置 cookie、自定义流),注入 @Res():
typescriptimport { Response } from 'express'; @Get('download') download(@Res() res: Response) { res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', 'attachment; filename=report.pdf'); fs.createReadStream('report.pdf').pipe(res); }
注意:一旦注入 @Res(),NestJS 不再自动序列化返回值——你必须自己调 res.json() 或 res.send()。如果只想设 cookie 但仍然用自动序列化,用 @Res({ passthrough: true }):
typescript@Post('login') login(@Body() dto: LoginDto, @Res({ passthrough: true }) res: Response) { const token = this.authService.login(dto); res.cookie('jwt', token, { httpOnly: true }); // 设 cookie return { message: '登录成功' }; // 返回值照常自动序列化 }
异步路由
NestJS 天然支持 async/await,返回 Promise 就行:
typescript@Get() async findAll(): Promise<User[]> { return this.usersService.findAll(); // service 返回 Promise }
也可以返回 RxJS Observable:
typescript@Get() findAll(): Observable<User[]> { return from(this.usersService.findAll()); }
路由版本控制
API 版本升级时,同一接口需要同时支持 v1 和 v2:
typescript// main.ts 启用版本控制 app.enableVersioning({ type: VersioningType.URI });
typescript@Controller('users') export class UsersController { @Get({ version: '1' }) // GET /v1/users findAllV1() { return this.usersService.findAllV1(); } @Get({ version: '2' }) // GET /v2/users findAllV2() { return this.usersService.findAllV2(); } }
也可以用枚举或数组支持多个版本:version: ['1', '2']。
请求生命周期
一个请求进入控制器前后的完整链路:
shell请求 → 中间件 → 守卫 → 拦截器(before) → 管道 → 控制器方法 → 拦截器(after) → 异常过滤器 → 响应
控制器方法抛出的异常会被异常过滤器捕获。如果没有自定义过滤器,NestJS 内置的异常过滤器返回标准 JSON 错误:
json{ "statusCode": 404, "message": "User not found" }
常见坑
路由顺序:NestJS 按声明顺序匹配路由。@Get(':id') 在 @Get('profile') 前面的话,/users/profile 会被 :id 匹配,id 值变成 "profile"。把具体路由放在参数路由前面:
typescript@Controller('users') export class UsersController { @Get('profile') // ✅ 具体路由在前 getProfile() {} @Get(':id') // 参数路由在后 findOne(@Param('id') id: string) {} }
返回 undefined:控制器方法返回 undefined 时,NestJS 返回空响应体和 200 状态码。如果你期望 204,要显式 @HttpCode(204)。
@Body() 拿不到数据:需要全局启用 ValidationPipe 或确保 app.useBodyParser() 没被禁用。