Skip to content
第 2 章 后端 ⏱ 12 分钟阅读

第 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 数组

动手练习 ​

  1. Articles Controller:写一个 ArticlesController,有 list/create/detail/update/remove 五个方法
  2. 自定义 Header:在 list 上加 @Header('X-Total-Count', '100')
  3. 404 测试:故意漏掉 Module 注册,启动看 404,然后修好

下一章:第 3 章:路由与参数 →

本站基于 VitePress 构建 · 由 StackHub 团队维护