6月4日 15:45

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 等场景需要流式返回:

typescript
import { Observable } from 'rxjs'; @Get('stream') streamData(): Observable<MessageEvent> { return interval(1000).pipe( map(() => ({ data: `Time: ${new Date().toISOString()}` })), ); }

返回 Observable 或 Stream 时,NestJS 自动处理背压和清理。

手动操作 Response

需要完全控制响应时(如设置 cookie、自定义流),注入 @Res()

typescript
import { 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() 没被禁用。

标签:NestJS