首页 / NestJS 入门教程 / 控制器 Controller

NestJS 入门教程

控制器 Controller

本教程共 47 篇 · 第 6 篇 · 更新于 2026-08-09 · 约 11 分钟阅读

NestJSController路由装饰器请求参数响应处理CRUD

本节目标:掌握控制器的全部用法——定义路由、提取参数、处理响应、异步操作,写出一个完整的 CRUD 控制器。

控制器是干嘛的

控制器负责接收 HTTP 请求,然后返回响应。

你可以把它理解成餐厅的服务员——客人(客户端)来了,服务员记下客人要什么(解析请求),然后告诉厨房(Service)去做,最后把菜端上来(返回响应)。

控制器本身不干活,它只做两件事:

  1. 接收请求,提取参数
  2. 调用服务处理,返回结果

定义一个控制器

@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 不关心你叫什么。findAllfindOnecreate 只是约定俗成的命名。

路由路径拼接规则

路由路径 = 控制器前缀 + 方法装饰器路径。

@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/123abcd/abc 都能匹配到。

Tip

带参数的路由(如 :id)要写在静态路由后面。不然 /users/profile 会被 :id 截走,profile 被当成 id 的值。

提取请求参数

控制器里最常用的操作就是提取请求数据。NestJS 提供了一组装饰器来搞定这件事。

@Param() — 路径参数

@Get(':id')
findOne(@Param('id') id: string) {
  return `用户 ID: ${id}`;
}

访问 GET /users/42id 的值就是 '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=10page1limit10

也可以一次拿到整个 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;
}
Tip

DTO 建议用 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。