第 2 章:控制器
学习目标
- 理解 Controller 的职责
- 掌握路由装饰器
- 学会返回 JSON、状态码、Header
- 避开 3 个常见路由坑
一、Controller 是干嘛的
Controller 只负责接收 HTTP 请求 → 调 Service → 返回响应,不写业务逻辑。业务逻辑全在 Service 里。
typescript
// users.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';
@Controller('users') // 路径前缀 /users
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get() // GET /users
findAll() {
return this.usersService.findAll();
}
@Post() // POST /users
create(@Body() dto: CreateUserDto) {
return this.usersService.create(dto);
}
}⚠️ 坑 1:在 Controller 里直接
repo.save()→ Controller 变胖、不可测 → 永远只调 Service。
二、HTTP 方法装饰器
typescript
import { Controller, Get, Post, Put, Patch, Delete, Options, Head } from '@nestjs/common';
@Controller('articles')
export class ArticlesController {
@Get() list() {}
@Get(':id') detail() {}
@Post() create() {}
@Put(':id') replace() {} // 全量替换
@Patch(':id') update() {} // 部分更新
@Delete(':id') remove() {}
@Options() options() {} // CORS 预检
@Head() head() {}
}三、路径前缀和动态参数
typescript
@Controller({ path: 'api/v1/users', host: ':tenant.example.com' })
// 多租户:不同 host 走同一个 Controller
export class UsersController {
@Get(':id')
findOne(@Param('id') id: string) {
return this.usersService.findOne(id);
}
}匹配:GET http://acme.example.com/api/v1/users/42。
多租户实战:@HostParam 拿子域名占位符:
typescript
import { Controller, Get, HostParam } from '@nestjs/common'
@Controller({ path: 'users', host: ':tenant.api.com' })
export class UsersController {
@Get()
findAll(@HostParam('tenant') tenant: string) {
// tenant = 子域名,如 'acme' / 'globex'
const db = getTenantDB(tenant) // 不同租户走不同数据库
return db.users.find()
}
}
// 请求:
// GET acme.api.com/users → tenant = 'acme',查 acme 库
// GET globex.api.com/users → tenant = 'globex',查 globex 库💡
@Controller({ path, host })= 给整个类加"路径前缀 + 子域名"——多租户用 host,API 版本用 path(v1/v2),@Param('id')拿动态参数——子域名占位符:tenant用@HostParam('tenant')拿。
四、返回类型控制
4.1 默认 JSON
NestJS 自动把对象/数组序列化成 JSON。
typescript
@Get(':id')
findOne(@Param('id') id: string) {
return { id, name: 'Tom' }; // 自动 JSON 化,Content-Type: application/json
}4.2 返回状态码
typescript
import { HttpCode } from '@nestjs/common';
@Post()
@HttpCode(201) // 默认 POST 是 201,可以覆盖
create() { return {}; }4.3 返回 Response 对象(完全控制)
typescript
import { Res } from '@nestjs/common';
import { Response } from 'express';
@Get('file')
download(@Res() res: Response) {
res.download('/tmp/report.pdf'); // 用 Express API
return; // ⚠️ 必须 return
}⚠️ 坑 2:用了
@Res()或@Response(),NestJS 就放弃返回值控制权,你必须自己res.send()。
4.4 Header
typescript
import { Header } from '@nestjs/common';
@Get()
@Header('Cache-Control', 'no-store')
findAll() { return []; }五、async 异步方法
Controller 方法可以是 async,NestJS 会等 Promise resolve 后再返回。
typescript
@Get()
async findAll(): Promise<User[]> {
return this.usersService.findAll(); // 自动 await
}
@Get()
async findAllObs(): Observable<User[]> {
return this.usersService.findAll$(); // Observable 也支持
}六、请求对象
typescript
import { Req } from '@nestjs/common';
import { Request } from 'express';
@Get()
handler(@Req() req: Request) {
console.log(req.method, req.url, req.headers);
}推荐精确取值,而不是整个 req:
typescript
import { Ip, HostParam, Headers } from '@nestjs/common';
@Get()
handler(
@Ip() ip: string, // 客户端 IP
@HostParam() host: string, // 域名
@Headers('user-agent') ua: string, // 指定 header
) {
return { ip, host, ua };
}七、Controller 必须注册到 Module
typescript
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { UsersService } from './users.service';
@Module({
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}⚠️ 坑 3:Controller 写了但
controllers: []没加 → 404,接口根本没注册。
八、本章小结
| 要点 | 关键 |
|---|---|
| 职责 | Controller 只路由,不写业务 |
| 装饰器 | @Controller(path) + @Get/@Post/... |
| 返回值 | 对象自动 JSON,@HttpCode 改状态码 |
| 响应控制 | @Res() 拿到底层,但要自己 res.send |
| 请求对象 | 优先用 @Ip/@Headers/@Param 而非整个 @Req |
| 注册 | 必须加进 Module 的 controllers 数组 |
动手练习
- Articles Controller:写一个
ArticlesController,有list/create/detail/update/remove五个方法 - 自定义 Header:在
list上加@Header('X-Total-Count', '100') - 404 测试:故意漏掉 Module 注册,启动看 404,然后修好
下一章:第 3 章:路由与参数 →