控制器 Controller
本教程共 47 篇 · 第 6 篇 · 更新于 2026-08-09 · 约 11 分钟阅读
本节目标:掌握控制器的全部用法——定义路由、提取参数、处理响应、异步操作,写出一个完整的 CRUD 控制器。
控制器是干嘛的
控制器负责接收 HTTP 请求,然后返回响应。
你可以把它理解成餐厅的服务员——客人(客户端)来了,服务员记下客人要什么(解析请求),然后告诉厨房(Service)去做,最后把菜端上来(返回响应)。
控制器本身不干活,它只做两件事:
- 接收请求,提取参数
- 调用服务处理,返回结果
定义一个控制器
用 @Controller() 装饰器标记一个类为控制器:
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(): string {
return '返回所有猫咪';
}
}
@Controller('cats') 里的 'cats' 是路由前缀。意思是这个控制器下所有路由都以 /cats 开头。
@Get() 标记这个方法处理 GET 请求。
路由 = 前缀 + 方法装饰器里的路径。这里前缀是 cats,方法没指定路径,所以最终路由是 GET /cats。
Tip用 CLI 创建控制器:
nest g controller cats。会自动生成文件,但你还需要在模块的controllers里注册它。
路由方法装饰器
NestJS 提供了所有标准 HTTP 方法对应的装饰器:
| 装饰器 | HTTP 方法 | 典型用途 |
|---|---|---|
@Get() | GET | 获取资源 |
@Post() | POST | 创建资源 |
@Put() | PUT | 完整更新 |
@Patch() | PATCH | 部分更新 |
@Delete() | DELETE | 删除资源 |
@All() | 所有方法 | 匹配所有 HTTP 方法 |
一个典型的 CRUD 控制器长这样:
@Controller('cats')
export class CatsController {
@Post()
create() {
return '创建一只新猫';
}
@Get()
findAll() {
return '返回所有猫';
}
@Get(':id')
findOne(@Param('id') id: string) {
return `返回第 ${id} 只猫`;
}
@Patch(':id')
update(@Param('id') id: string) {
return `部分更新第 ${id} 只猫`;
}
@Delete(':id')
remove(@Param('id') id: string) {
return `删除第 ${id} 只猫`;
}
}
Note方法名随便取,NestJS 不关心你叫什么。
findAll、findOne、create只是约定俗成的命名。
路由路径拼接规则
路由路径 = 控制器前缀 + 方法装饰器路径。
@Controller('users')
export class UsersController {
@Get() // GET /users
findAll() {}
@Get('profile') // GET /users/profile
getProfile() {}
@Get('admin/all') // GET /users/admin/all
findAllAdmin() {}
}
还支持通配符:
@Get('abcd/*')
findAll() {
return '匹配 abcd/ 后面的任意路径';
}
abcd/、abcd/123、abcd/abc 都能匹配到。
Tip带参数的路由(如
:id)要写在静态路由后面。不然/users/profile会被:id截走,profile被当成 id 的值。
提取请求参数
控制器里最常用的操作就是提取请求数据。NestJS 提供了一组装饰器来搞定这件事。
@Param() — 路径参数
@Get(':id')
findOne(@Param('id') id: string) {
return `用户 ID: ${id}`;
}
访问 GET /users/42,id 的值就是 '42'。
多个路径参数也行:
@Get(':userId/posts/:postId')
getUserPost(
@Param('userId') userId: string,
@Param('postId') postId: string,
) {
return `用户 ${userId} 的文章 ${postId}`;
}
或者一次拿到所有参数:
@Get(':id')
findOne(@Param() params: { id: string }) {
return `用户 ID: ${params.id}`;
}
@Query() — 查询参数
@Get()
findAll(
@Query('page') page: number,
@Query('limit') limit: number,
) {
return `第 ${page} 页,每页 ${limit} 条`;
}
访问 GET /users?page=1&limit=10,page 是 1,limit 是 10。
也可以一次拿到整个 query 对象:
@Get()
findAll(@Query() query: { page?: number; limit?: number }) {
return query;
}
@Body() — 请求体
@Post()
create(@Body() createUserDto: CreateUserDto) {
return this.usersService.create(createUserDto);
}
也可以提取请求体中的某个字段:
@Post('profile')
updateProfile(
@Body('name') name: string,
@Body('email') email: string,
) {
return { name, email };
}
@Headers() — 请求头
@Get()
findAll(@Headers('authorization') auth: string) {
return `Token: ${auth}`;
}
@Ip() — 客户端 IP
@Get()
findAll(@Ip() ip: string) {
return `你的 IP: ${ip}`;
}
装饰器汇总
| 装饰器 | 提取的数据 |
|---|---|
@Req() / @Request() | 原始请求对象 |
@Res() / @Response() | 原始响应对象 |
@Param(key?) | 路径参数 |
@Query(key?) | 查询参数 |
@Body(key?) | 请求体 |
@Headers(name?) | 请求头 |
@Ip() | 客户端 IP |
@Session() | Session 对象 |
@HostParam() | Host 参数 |
Tip大部分情况下用
@Param()、@Query()、@Body()就够了。@Req()和@Res()是”逃生舱”,需要直接操作底层对象时才用。
@Req() 直接拿请求对象
如果你需要访问请求对象上的某些属性,可以直接注入:
import { Controller, Get, Req } from '@nestjs/common';
import type { Request } from 'express';
@Controller('cats')
export class CatsController {
@Get()
findAll(@Req() request: Request): string {
console.log(request.query);
console.log(request.headers);
return '返回所有猫';
}
}
Note装
@types/express才能获得Request类型提示:npm install -D @types/express。
响应处理
NestJS 有两种处理响应的方式。
标准方式(推荐)
直接 return 就行。NestJS 会自动帮你处理:
- 返回对象或数组 → 自动序列化成 JSON
- 返回字符串或数字 → 直接发送
- 默认状态码 200,POST 请求默认 201
@Get()
findAll() {
return { message: 'Success', data: [] };
// 响应: {"message":"Success","data":[]}
}
简单、干净、不用操心。
库特定方式
用 @Res() 注入底层的响应对象,自己控制响应:
import { Controller, Get, Res } from '@nestjs/common';
import type { Response } from 'express';
@Controller('cats')
export class CatsController {
@Get()
findAll(@Res() res: Response) {
res.status(200).json({
message: 'Success',
data: [],
});
}
}
Warning用了
@Res()之后,你必须自己调用res.json()或res.send()来返回响应。不调用的话请求会一直挂着。
而且用了 @Res() 之后,NestJS 的标准响应机制就失效了——@HttpCode()、@Header() 这些装饰器都不管用了。
如果你只想用 @Res() 设置个 Cookie 或 Header,但还想让 NestJS 处理响应体,可以加 passthrough 选项:
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.setHeader('X-Custom', 'value');
return { data: [] }; // NestJS 照常处理
}
Tip日常开发用标准方式就够了。只有需要设置 Cookie、流式响应等特殊场景时才用
@Res()。
设置状态码
默认 GET 返回 200,POST 返回 201。想改状态码用 @HttpCode():
import { Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
@HttpCode(HttpStatus.CREATED) // 201
create() {
return '创建成功';
}
@Post()
@HttpCode(204) // 无内容
noContent() {
return;
}
}
设置响应头
用 @Header() 装饰器:
@Get()
@Header('Cache-Control', 'no-store')
@Header('X-Custom-Header', 'hello')
findAll() {
return '返回数据';
}
可以叠加多个 @Header()。
重定向
用 @Redirect() 装饰器:
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs() {}
也可以动态决定重定向地址:
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version: string) {
if (version === '5') {
return { url: 'https://docs.nestjs.com/v5/' };
}
}
方法返回的对象会覆盖 @Redirect() 里的参数。
异步处理
控制器方法天然支持 async/await:
@Get()
async findAll(): Promise<any[]> {
return await this.catsService.findAll();
}
也支持返回 RxJS Observable:
import { Observable, of } from 'rxjs';
@Get()
findAll(): Observable<any[]> {
return of([{ name: 'Tom' }]);
}
NestJS 会自动订阅 Observable,拿到最终值后返回给客户端。
Note大部分场景用 async/await 就够了。RxJS Observable 适合需要流式处理、组合多个异步操作的场景。
DTO:定义请求体结构
用 TypeScript 类来定义请求体的结构,这叫 DTO(Data Transfer Object):
// create-cat.dto.ts
export class CreateCatDto {
name: string;
age: number;
breed: string;
}
在控制器里使用:
@Post()
async create(@Body() createCatDto: CreateCatDto) {
return createCatDto;
}
TipDTO 建议用 class 而不是 interface。因为 interface 编译后就没了,运行时拿不到类型信息。class 编译后还在,Pipe 做数据验证时需要用到。
控制器注册
控制器写好后,必须在模块里注册才能生效:
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule {}
不注册的话 NestJS 不知道有这个控制器,路由也不会生效。
控制器作用域
控制器默认是单例的——整个应用只有一个实例。这在绝大多数情况下没问题。
特殊场景下可以改成每个请求一个实例:
import { Controller, Scope } from '@nestjs/common';
@Controller({ path: 'cats', scope: Scope.REQUEST })
export class CatsController {}
| 作用域 | 说明 |
|---|---|
DEFAULT(默认) | 单例,所有请求共享一个实例 |
REQUEST | 每个请求创建新实例 |
TRANSIENT | 每次注入都创建新实例 |
Note
REQUEST作用域会影响性能,因为每个请求都要创建新实例。只有在需要请求级别的状态隔离时才用,比如多租户场景。
完整 CRUD 控制器示例
把所有知识点串起来:
import {
Controller, Get, Post, Put, Patch, Delete,
Body, Param, HttpCode, HttpStatus,
} from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
@HttpCode(HttpStatus.CREATED)
create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}
@Get()
findAll() {
return this.catsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.catsService.findOne(+id);
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() updateCatDto: UpdateCatDto,
) {
return this.catsService.update(+id, updateCatDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.catsService.remove(+id);
}
}
这就是一个标准的 RESTful 控制器。增删改查,清清楚楚。
小结
控制器是 NestJS 里最直接的部分——处理请求、返回响应。
核心知识点:
@Controller()定义控制器和路由前缀@Get()@Post()等定义路由方法@Param()@Query()@Body()提取请求数据- 直接
return就行,NestJS 自动处理响应 - 控制器必须在模块里注册
- DTO 用 class 定义,别用 interface
控制器只负责”接活”,真正的业务逻辑写在 Service 里。下一章就来聊聊 Service 和 Provider。