Skip to content

控制器

控制器负责处理传入的请求并向客户端发送响应

控制器的目的是处理应用的特定请求。路由机制决定哪个控制器处理哪个请求。通常,一个控制器有多个路由,每个路由可以执行不同的操作。

要创建基本的控制器,我们使用类和装饰器。装饰器将类与必要的元数据关联起来,使 Nest 能够创建路由映射,将请求连接到相应的控制器。

提示

要快速创建一个带有内置验证的 CRUD 控制器,你可以使用 CLI 的 CRUD 生成器nest g resource [name]

路由

在下面的示例中,我们将使用 @Controller() 装饰器,这是定义基本控制器所必需的。我们将指定一个可选的路由路径前缀 cats。在 @Controller() 装饰器中使用路径前缀可以帮助我们将相关路由分组在一起,减少重复代码。例如,如果我们想将管理猫实体交互的路由分组到 /cats 路径下,我们可以在 @Controller() 装饰器中指定 cats 路径前缀。这样,我们就不需要在文件中的每个路由中重复该路径部分。

typescript
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

提示

要使用 CLI 创建控制器,只需执行 $ nest g controller [name] 命令。

@Get() HTTP 请求方法装饰器放在 findAll() 方法之前,告诉 Nest 为特定的 HTTP 请求端点创建处理程序。该端点由 HTTP 请求方法(本例中为 GET)和路由路径定义。那么,路由路径是什么?处理程序的路由路径是通过将控制器声明的(可选)前缀与方法装饰器中指定的任何路径组合来确定的。由于我们为每个路由设置了前缀(cats),并且没有在方法装饰器中添加任何特定路径,Nest 会将 GET /cats 请求映射到此处理程序。

如前所述,路由路径包括可选的控制器路径前缀方法装饰器中指定的任何路径字符串。例如,如果控制器前缀是 cats,方法装饰器是 @Get('breed'),则生成的路由将是 GET /cats/breed

在上面的示例中,当对此端点发出 GET 请求时,Nest 将请求路由到用户定义的 findAll() 方法。请注意,我们在这里选择的方法名完全是任意的。虽然我们必须声明一个方法来绑定路由,但 Nest 不会对方法名赋予任何特殊含义。

此方法将返回 200 状态码和关联的响应,在本例中只是一个字符串。为什么会这样?为了解释这一点,我们首先需要介绍 Nest 使用两种不同选项来操作响应的概念:

标准(推荐)使用此内置方法,当请求处理程序返回 JavaScript 对象或数组时,它将自动序列化为 JSON。当返回 JavaScript 原始类型(例如 stringnumberboolean)时,Nest 将只发送值而不尝试序列化。这使得响应处理变得简单:只需返回值,Nest 会处理其余部分。此外,响应的状态码默认始终为 200,POST 请求除外,它使用 201。我们可以通过在处理程序级别添加 @HttpCode(...) 装饰器来轻松更改此行为(参见状态码)。
特定库方式我们可以使用特定库(例如 Express)的响应对象,可以使用 @Res() 装饰器在方法处理程序签名中注入(例如 findAll(@Res() response))。使用此方法,你可以使用该对象暴露的原生响应处理方法。例如,使用 Express,你可以使用 response.status(200).send() 这样的代码构建响应。

警告

Nest 会检测处理程序何时使用 @Res()@Next(),表明你选择了特定库选项。如果同时使用两种方法,标准方法将自动禁用该单个路由,并且不再按预期工作。要同时使用两种方法(例如,注入响应对象仅用于设置 cookies/headers,但仍将其余部分留给框架),你必须在 @Res({ passthrough: true }) 装饰器中将 passthrough 选项设置为 true

请求对象

处理程序通常需要访问客户端的请求详情。Nest 提供了对底层平台(默认为 Express)的请求对象的访问。你可以通过在处理程序签名中使用 @Req() 装饰器来指示 Nest 注入请求对象。

typescript
import { Controller, Get, Req } from '@nestjs/common';
import type { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}

提示

要利用 express 类型(如上面的 request: Request 参数示例),请确保安装 @types/express 包。

请求对象表示 HTTP 请求,包含查询字符串、参数、HTTP 头和请求体的属性(在这里阅读更多)。在大多数情况下,你不需要手动访问这些属性。相反,你可以使用开箱即用的专用装饰器,如 @Body()@Query()。以下是提供的装饰器及其对应的平台特定对象的列表。

装饰器对应对象
@Request(), @Req()req
@Response(), @Res()res
@Next()next
@Session()req.session
@Param(key?: string)req.params / req.params[key]
@Body(key?: string)req.body / req.body[key]
@Query(key?: string)req.query / req.query[key]
@Headers(name?: string)req.headers / req.headers[name]
@Ip()req.ip
@HostParam()req.hosts

为了与底层 HTTP 平台(例如 Express 和 Fastify)的类型兼容,Nest 提供了 @Res()@Response() 装饰器。@Res() 只是 @Response() 的别名。两者都直接暴露底层原生平台的 response 对象接口。使用它们时,你还应该导入底层库的类型(例如 @types/express)以充分利用。请注意,当你在方法处理程序中注入 @Res()@Response() 时,你将 Nest 置于该处理程序的特定库模式,你将负责管理响应。这样做时,你必须通过调用 response 对象发出某种响应(例如 res.json(...)res.send(...)),否则 HTTP 服务器将挂起。

提示

要了解如何创建自定义装饰器,请访问这个章节。

资源

之前,我们定义了一个获取猫资源的端点(GET 路由)。我们通常还需要提供一个创建新记录的端点。为此,让我们创建 POST 处理程序:

typescript
import { Controller, Get, Post } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Post()
  create(): string {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}

就是这么简单。Nest 为所有标准 HTTP 方法提供了装饰器:@Get()@Post()@Put()@Delete()@Patch()@Options()@Head()。此外,@All() 定义了一个处理所有方法的端点。

路由通配符

NestJS 也支持基于模式的路由。例如,星号(*)可以用作通配符来匹配路径末尾的任意字符组合。在下面的示例中,findAll() 方法将对以 abcd/ 开头的任何路由执行,无论后面跟着多少个字符。

typescript
@Get('abcd/*')
findAll() {
  return 'This route uses a wildcard';
}

'abcd/*' 路由路径将匹配 abcd/abcd/123abcd/abc 等。连字符(-)和点(.)在基于字符串的路径中按字面意义解释。

状态码

如前所述,响应的默认状态码始终为 200,POST 请求除外,默认为 201。你可以通过在处理程序级别使用 @HttpCode(...) 装饰器来轻松更改此行为。

typescript
@Post()
@HttpCode(204)
create() {
  return 'This action adds a new cat';
}

提示

@nestjs/common 包导入 HttpCode

通常,你的状态码不是静态的,而是取决于各种因素。在这种情况下,你可以使用特定库的响应对象(使用 @Res() 注入),或者在出错时抛出异常。

响应头

要指定自定义响应头,你可以使用 @Header() 装饰器或特定库的响应对象(直接调用 res.header())。

typescript
@Post()
@Header('Cache-Control', 'no-store')
create() {
  return 'This action adds a new cat';
}

提示

@nestjs/common 包导入 Header

重定向

要将响应重定向到特定 URL,你可以使用 @Redirect() 装饰器或特定库的响应对象(直接调用 res.redirect())。

@Redirect() 接受两个参数,urlstatusCode,两者都是可选的。如果省略,statusCode 的默认值为 302Found)。

typescript
@Get()
@Redirect('https://nestjs.com', 301)

提示

有时你可能想动态确定 HTTP 状态码或重定向 URL。通过返回一个遵循 HttpRedirectResponse 接口(来自 @nestjs/common)的对象来实现。

返回的值将覆盖传递给 @Redirect() 装饰器的任何参数。例如:

typescript
@Get('docs')
@Redirect('/guide/introduction', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: '/application-context' };
  }
}

路由参数

当你需要接受动态数据作为请求的一部分时(例如 GET /cats/1 获取 id 为 1 的猫),静态路径的路由将无法工作。要定义带参数的路由,你可以在路由路径中添加路由参数标记来捕获 URL 中的动态值。下面 @Get() 装饰器示例中的路由参数标记演示了这种方法。然后可以使用 @Param() 装饰器访问这些路由参数,该装饰器应添加到方法签名中。

提示

带参数的路由应在任何静态路径之后声明。这可以防止参数化路径拦截发往静态路径的流量。

typescript
@Get(':id')
findOne(@Param() params: any): string {
  console.log(params.id);
  return `This action returns a #${params.id} cat`;
}

@Param() 装饰器用于装饰方法参数(上面示例中的 params),使路由参数作为该装饰方法参数的属性在方法内部可访问。如代码所示,你可以通过引用 params.id 来访问 id 参数。或者,你可以向装饰器传递特定的参数标记,并在方法体中直接按名称引用路由参数。

提示

@nestjs/common 包导入 Param

typescript
@Get(':id')
findOne(@Param('id') id: string): string {
  return `This action returns a #${id} cat`;
}

子域路由

@Controller 装饰器可以接受一个 host 选项,要求传入请求的 HTTP 主机匹配某个特定值。

typescript
@Controller({ host: 'admin.example.com' })
export class AdminController {
  @Get()
  index(): string {
    return 'Admin page';
  }
}

警告

由于 Fastify 不支持嵌套路由器,如果你使用子域路由,建议使用默认的 Express 适配器。

与路由 path 类似,host 选项可以使用标记来捕获主机名中该位置的动态值。@Controller() 装饰器示例中的主机参数标记演示了这种用法。以这种方式声明的主机参数可以使用 @HostParam() 装饰器访问,该装饰器应添加到方法签名中。

typescript
@Controller({ host: ':account.example.com' })
export class AccountController {
  @Get()
  getInfo(@HostParam('account') account: string) {
    return account;
  }
}

状态共享

对于来自其他编程语言的开发者来说,可能会惊讶地发现在 Nest 中,几乎所有东西都在传入请求之间共享。这包括数据库连接池、具有全局状态的单例服务等资源。重要的是要理解,Node.js 不使用请求/响应多线程无状态模型,其中每个请求由单独的线程处理。因此,在 Nest 中使用单例实例对我们的应用来说是完全安全的。

也就是说,在某些特定的边缘情况下,可能需要基于请求的控制器生命周期。例如 GraphQL 应用中的按请求缓存、请求跟踪或实现多租户。你可以在这里了解更多关于控制注入作用域的信息。

异步性

我们热爱现代 JavaScript,特别是它对异步数据处理的强调。这就是为什么 Nest 完全支持 async 函数。每个 async 函数都必须返回一个 Promise,这允许你返回一个延迟值,Nest 可以自动解析。这是一个示例:

typescript
@Get()
async findAll(): Promise<any[]> {
  return [];
}

这段代码完全有效。但 Nest 更进一步,允许路由处理程序返回 RxJS 可观察流。Nest 将在内部处理订阅,并在流完成后解析最终发出的值。

typescript
@Get()
findAll(): Observable<any[]> {
  return of([]);
}

两种方法都有效,你可以选择最适合你需求的方式。

请求负载

在之前的示例中,POST 路由处理程序不接受任何客户端参数。让我们通过添加 @Body() 装饰器来修复这个问题。

在继续之前(如果你使用 TypeScript),我们需要定义 DTO(数据传输对象)模式。DTO 是一个指定数据应如何通过网络发送的对象。我们可以使用 TypeScript 接口或简单的类来定义 DTO 模式。然而,我们在这里推荐使用。为什么?类是 JavaScript ES6 标准的一部分,因此它们在编译后的 JavaScript 中作为真实实体保持完整。相比之下,TypeScript 接口在转译过程中会被移除,这意味着 Nest 无法在运行时引用它们。这很重要,因为像管道这样的功能依赖于在运行时访问变量的元类型,这只有使用类才能实现。

让我们创建 CreateCatDto 类:

typescript
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

它只有三个基本属性。之后我们可以在 CatsController 中使用新创建的 DTO:

typescript
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}

提示

我们的 ValidationPipe 可以过滤掉不应被方法处理程序接收的属性。在这种情况下,我们可以将可接受的属性列入白名单,任何不在白名单中的属性将自动从结果对象中剥离。在 CreateCatDto 示例中,我们的白名单是 nameagebreed 属性。在这里了解更多。

查询参数

在路由中处理查询参数时,你可以使用 @Query() 装饰器从传入请求中提取它们。让我们看看实际操作。

考虑一个路由,我们想根据 agebreed 等查询参数过滤猫的列表。首先,在 CatsController 中定义查询参数:

typescript
@Get()
async findAll(@Query('age') age: number, @Query('breed') breed: string) {
  return `This action returns all cats filtered by age: ${age} and breed: ${breed}`;
}

在此示例中,@Query() 装饰器用于从查询字符串中提取 agebreed 的值。例如,请求:

plaintext
GET /cats?age=2&breed=Persian

将导致 age2breedPersian

如果你的应用需要处理更复杂的查询参数,例如嵌套对象或数组:

plaintext
?filter[where][name]=John&filter[where][age]=30
?item[]=1&item[]=2

你需要配置 HTTP 适配器(Express 或 Fastify)使用适当的查询解析器。在 Express 中,你可以使用 extended 解析器,它允许丰富的查询对象:

typescript
const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('query parser', 'extended');

在 Fastify 中,你可以使用 querystringParser 选项:

typescript
const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter({
    querystringParser: (str) => qs.parse(str),
  }),
);

提示

qs 是一个支持嵌套和数组的查询字符串解析器。你可以使用 npm install qs 安装它。

错误处理

有一个单独的章节介绍错误处理(即处理异常),请参阅这里

完整资源示例

下面是一个示例,演示了使用多个可用装饰器创建基本控制器。此控制器提供了几种方法来访问和操作内部数据。

typescript
import { Controller, Get, Query, Post, Body, Put, Param, Delete } from '@nestjs/common';
import { CreateCatDto, UpdateCatDto, ListAllEntities } from './dto';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Body() createCatDto: CreateCatDto) {
    return 'This action adds a new cat';
  }

  @Get()
  findAll(@Query() query: ListAllEntities) {
    return `This action returns all cats (limit: ${query.limit} items)`;
  }

  @Get(':id')
  findOne(@Param('id') id: string) {
    return `This action returns a #${id} cat`;
  }

  @Put(':id')
  update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
    return `This action updates a #${id} cat`;
  }

  @Delete(':id')
  remove(@Param('id') id: string) {
    return `This action removes a #${id} cat`;
  }
}

提示

Nest CLI 提供了一个生成器(schematic),可以自动创建所有样板代码,省去手动操作并改善整体开发体验。在这里了解更多关于此功能的信息。

启动和运行

即使 CatsController 已完全定义,Nest 仍然不知道它的存在,也不会自动创建该类的实例。

控制器必须始终是模块的一部分,这就是为什么我们在 @Module() 装饰器中包含 controllers 数组。由于我们还没有定义除根 AppModule 之外的其他模块,我们将使用它来注册 CatsController

typescript
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

@Module({
  controllers: [CatsController],
})
export class AppModule {}

我们使用 @Module() 装饰器将元数据附加到模块类,现在 Nest 可以轻松确定需要挂载哪些控制器。

特定库方式

到目前为止,我们已经介绍了 Nest 操作响应的标准方式。另一种方式是使用特定库的响应对象。要注入特定的响应对象,我们可以使用 @Res() 装饰器。为了突出差异,让我们像这样重写 CatsController

typescript
import { Controller, Get, Post, Res, HttpStatus } from '@nestjs/common';
import { Response } from 'express';

@Controller('cats')
export class CatsController {
  @Post()
  create(@Res() res: Response) {
    res.status(HttpStatus.CREATED).send();
  }

  @Get()
  findAll(@Res() res: Response) {
     res.status(HttpStatus.OK).json([]);
  }
}

虽然这种方法可行,并且通过完全控制响应对象提供了更多灵活性(如头部操作和访问特定库功能),但应谨慎使用。通常,这种方法不够清晰,并且有一些缺点。主要缺点是你的代码变得依赖于平台,因为不同的底层库可能对响应对象有不同的 API。此外,它会使测试更具挑战性,因为你需要模拟响应对象等。

此外,使用这种方法,你将失去与依赖标准响应处理的 Nest 功能的兼容性,如拦截器和 @HttpCode() / @Header() 装饰器。要解决这个问题,你可以像这样启用 passthrough 选项:

typescript
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
  res.status(HttpStatus.OK);
  return [];
}

使用这种方法,你可以与原生响应对象交互(例如,根据特定条件设置 cookies 或 headers),同时仍然让框架处理其余部分。

基于 NestJS 官方文档翻译