Overview

控制器

学习如何在 NestJS 中创建和使用控制器来处理 HTTP 请求和响应

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

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

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

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

路由

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

@@filename(cats.controller)
import { Controller, Get } from '@nestjs/common';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(): string {
    return 'This action returns all cats';
  }
}
@@switch
import { Controller, Get } from '@nestjs/common';

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

info 提示 要使用 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() 的代码构造响应。

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

请求对象

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

@@filename(cats.controller)
import { Controller, Get, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller('cats')
export class CatsController {
  @Get()
  findAll(@Req() request: Request): string {
    return 'This action returns all cats';
  }
}
@@switch
import { Controller, Bind, Get, Req } from '@nestjs/common';

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

info 提示 要利用 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 服务器将挂起。

info 提示 要了解如何创建自己的自定义装饰器,请访问章节。

资源

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

@@filename(cats.controller)
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';
  }
}
@@switch
import { Controller, Get, Post } from '@nestjs/common';

@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 根本不支持它们。

Status code

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

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

info 提示@nestjs/common 包导入 HttpCode

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

响应头

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

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

info 提示@nestjs/common 包导入 Header

重定向

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

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

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

info 提示 有时您可能希望动态确定 HTTP 状态码或重定向 URL。通过返回遵循 HttpRedirectResponse 接口(来自 @nestjs/common)的对象来实现。

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

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

路由参数

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

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

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

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

info 提示@nestjs/common 包导入 Param

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

子域路由

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

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

warning 警告 由于 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 可以自动解析的延迟值。这是一个示例:

@@filename(cats.controller)
@Get()
async findAll(): Promise<any[]> {
  return [];
}
@@switch
@Get()
async findAll() {
  return [];
}

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

@@filename(cats.controller)
@Get()
findAll(): Observable<any[]> {
  return of([]);
}
@@switch
@Get()
findAll() {
  return of([]);
}

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

请求负载

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

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

让我们创建 CreateCatDto 类:

@@filename(create-cat.dto)
export class CreateCatDto {
  name: string;
  age: number;
  breed: string;
}

It has only three basic properties. Thereafter we can use the newly created DTO inside the CatsController:

@@filename(cats.controller)
@Post()
async create(@Body() createCatDto: CreateCatDto) {
  return 'This action adds a new cat';
}
@@switch
@Post()
@Bind(Body())
async create(createCatDto) {
  return 'This action adds a new cat';
}

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

查询参数

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

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

@@filename(cats.controller)
@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),
  }),
);

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

错误处理

有一个关于错误处理(即,处理异常)的单独章节此处

完整资源示例

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

@@filename(cats.controller)
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`;
  }
}
@@switch
import { Controller, Get, Query, Post, Body, Put, Param, Delete, Bind } from '@nestjs/common';

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

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

启动和运行

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

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

@@filename(app.module)
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';

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

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

库特定方法

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

@@filename()
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([]);
  }
}
@@switch
import { Controller, Get, Post, Bind, Res, Body, HttpStatus } from '@nestjs/common';

@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 选项:

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

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