控制器

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

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

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

提示

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

路由

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

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

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

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

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

放在 findAll() 方法之前的 @Get() HTTP 请求方法装饰器告诉 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,除了使用 201 的 POST 请求。我们可以通过在处理程序级别添加 @HttpCode(...) 装饰器来轻松更改此行为(请参阅 状态码)。
库特定 我们可以使用库特定的(例如,Express)响应对象,可以使用方法处理程序签名中的 @Res() 装饰器来注入(例如,findAll(@Res() response))。通过这种方法,您可以使用该对象公开的本机响应处理方法。例如,使用 Express,您可以使用 response.status(200).send() 这样的代码构造响应。
警告

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

请求对象

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

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';
  }
}

@Controller('cats')
export class CatsController {
  @Get()
  @Bind(Req())
  findAll(request) {
    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 服务器将挂起。

提示

要学习如何创建自己的自定义装饰器,请访问 章节。

资源

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

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';
  }
}

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

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

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

路由通配符

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

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

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

这种方法在 Express 和 Fastify 上都有效。然而,随着 Express(v5)的最新版本,路由系统变得更加严格。在纯 Express 中,您必须使用命名通配符来使路由工作 - 例如,abcd/*splat,其中 splat 只是通配符参数的名称,没有特殊含义。您可以随意命名它。也就是说,由于 Nest 为 Express 提供了兼容层,您仍然可以使用星号(*)作为通配符。

当涉及到在路由 中间 使用的星号时,Express 需要命名通配符(例如,ab{{ '{' }}*splat}cd),而 Fastify 根本不支持它们。

状态码

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

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

@nestjs/common 包导入 HttpCode

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

响应头

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

@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)。

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

有时您可能希望动态确定 HTTP 状态码或重定向 URL。为此,返回一个遵循 HttpRedirectResponse 接口(来自 @nestjs/common)的对象。

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

@Get('docs')
@Redirect('./', 302)
getDocs(@Query('version') version) {
  if (version && version === '5') {
    return { url: '/v5/' };
  }
}

路由参数

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

提示

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

@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

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

子域路由

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

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

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

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

@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 可以自动解析的延迟值。这是一个例子:

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

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

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

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

请求体

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

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

让我们创建 CreateCatDto 类:

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

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

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

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

查询参数

在处理路由中的查询参数时,您可以使用 @Query() 装饰器从传入请求中提取它们。让我们看看这在实践中是如何工作的。

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

@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 的值。例如,对以下请求:

GET /cats?age=2&breed=Persian

会导致 age2breedPersian

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

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

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

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

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

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

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

错误处理

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

完整资源示例

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

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`;
  }
}

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

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

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

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

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

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

启动和运行

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

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

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

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

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

库特定方法

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

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([]);
  }
}

@Controller('cats')
export class CatsController {
  @Post()
  @Bind(Res(), Body())
  create(res, createCatDto) {
    res.status(HttpStatus.CREATED).send();
  }

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

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

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

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

通过这种方法,您可以与本机响应对象交互(例如,根据特定条件设置 cookie 或标头),同时仍允许框架处理其余部分。