NestJS 🧑‍🍳 厨子必修课(三):控制器

517 阅读6分钟

1. 前言

同福客栈应该有一个菜单供客人点餐,每一道菜都有自己的信息(比如配料是什么、价格多少),配料和价格可以调整,可以不提供这道菜。

简而言之,菜单就是一个资源,RESTful 风格的 API 接口如下:

  • GET /menus 获取所有菜品
  • GET /menus/:id 获取某个菜品详情
  • POST /menus 添加新的菜品
  • PATCH /menus/:id 修改菜品详情
  • DELETE /menus/:id 删除某个菜品

欢迎加入技术交流群

image.png

  1. NestJS 🧑‍🍳  厨子必修课(一):后端的本质
  2. NestJS 🧑‍🍳 厨子必修课(二):项目创建
  3. NestJS 🧑‍🍳 厨子必修课(三):控制器
  4. NestJS 🧑‍🍳 厨子必修课(四):服务类
  5. NestJS 🧑‍🍳 厨子必修课(五):Prisma 集成(上)
  6. NestJS 🧑‍🍳 厨子必修课(六):Prisma 集成(下)
  7. NestJS 🧑‍🍳 厨子必修课(七):管道
  8. NestJS 🧑‍🍳 厨子必修课(八):异常过滤器

2. 快速生成 menus 资源

通过以下命令快速生成一个有关 menus 资源的控制器文件:

$ nest g resource menus

执行后会在 src 目录下生成一个有关 menus 资源的文件夹:

menus 资源.png

可以看到这组资源的构成与外层的 app 是类似的,都由 controller、service、module 文件构成。

先前说过“约定大于配置”,因此可以预见的是 controller 文件里应该写了 menus 相关的路由匹配以及对应执行的 service 方法,而 service 文件中包含了业务处理逻辑和具体响应给前端的内容,module 则负责如何组织它们。

3. 控制器

打开 menus.controller.ts 文件可以看到已经自动生成了相关代码,整体结构如下:

// 引入相关依赖...

@Controller('menus')
export class MenusController {
  // 相关代码...
}

在 Nest 中使用了大量的装饰器语法,可以理解为**“武器赋魔”,使之具备某种能力**。

@Controller 用于将类 MenusController 标记为控制器,控制器是用于处理传入的 HTTP 请求的类。它定义了应用程序的路由,并根据不同的 HTTP 方法(如 GETPOSTPUTDELETE 等)来处理请求。

@Controller('menus') 中的字符串参数 'menus' ****指定了控制器处理的基础路由路径,在这个例子中,所有与这个控制器相关的路由都会以 /menus 开头。

3.1 依赖注入

在控制器类 MenusController 中有如下代码:

// 依赖注入
constructor(private readonly menusService: MenusService) {}

@Get()
findAll() {
  // 使用服务类实例方法
  return this.menusService.findAll();
}

// 其他代码...

在 Nest 中有两个很重要的概念:

  • 控制反转(Inversion of Control,简称 IOC)
  • 依赖注入(Dependency Injection,简称 DI)

constructor(private readonly menusService: MenusService) {} :调用构造函数时,Nest 会通过 IOC 容器来自动实例化依赖项(这里指的是 MenusService:一个服务类,负责处理与菜单相关的业务逻辑),并自动将 MenusService 的实例注入到这个控制器类 MenusController 中。

这样一来,就能在控制类 MenusController 中通过 this.menusService 来调用 MenusService 中的实例方法了。

⚠️ 注意:通过 private readonly 声明,这个依赖只能在类内部使用,并且不能被修改。

依赖注入这种解耦设计不再需要将繁杂的业务逻辑写在控制器中,而是将依赖关系从内部转移到外部,这意味着可以根据需求自由添加或更换依赖项。

在这里可以说 MenusController 依赖 MenusServiceMenusServiceMenusController 的依赖项。

除了能够匹配到路由,下一步是要处理对应的 HTTP 请求(POST、GET、PATCH、DELETE)。

3.2 @Post()

@Post()
create(@Body() createMenuDto: CreateMenuDto) {
  return this.menusService.create(createMenuDto);
}
  • @Post():这个装饰器标识此方法处理 POST 请求。通常用于创建新的资源。
  • create(@Body() createMenuDto: CreateMenuDto):这个方法接收一个 createMenuDto 参数,表示请求体中的数据。@Body() 装饰器从 HTTP 请求的 body 中提取数据,并将其转换为 CreateMenuDto 对象。从这里可以看出:类中的方法是可以使用装饰器的,甚至是方法中的入参。
  • this.menusService.create(createMenuDto):调用 MenusService 实例的 create 方法,将 createMenuDto 传递给它。MenusService 将负责处理实际的创建逻辑。

有了这段代码,掌柜的可以添加新的菜品了:

POST请求.png

3.3 @Get()

@Get()
findAll() {
  return this.menusService.findAll();
}
  • @Get():这个装饰器标识此方法处理 GET 请求。通常用于获取资源的列表。
  • findAll():直接调用 MenusServicefindAll 方法来获取所有菜单。

有了这段代码,客人们可以查看所有的菜品了:

GET请求.png

3.4 @Get(':id')

@Get(':id')
findOne(@Param('id') id: string) {
  return this.menusService.findOne(+id);
}
  • @Get(':id'):这个装饰器标识此方法处理 GET 请求,并且路径中带有一个 id 参数。:id 是一个路径参数,表示特定资源的标识符。
  • findOne(@Param('id') id: string):这个方法使用 @Param('id') 从请求路径中提取 id 参数,并将其转换为字符串类型传递给 findOne 方法。
  • this.menusService.findOne(+id):调用 MenusServicefindOne 方法,传入的 id 参数前加上 + 表示将字符串 id 转换为数字。

有了这段代码,客人们可以查看某一道菜品的详情了:

GET请求详情.png

3.5 @Patch(':id')

@Patch(':id')
update(@Param('id') id: string, @Body() updateMenuDto: UpdateMenuDto) {
  return this.menusService.update(+id, updateMenuDto);
}
  • @Patch(':id'):这个装饰器标识此方法处理 PATCH 请求。PATCH 请求通常用于更新部分资源。
  • update(@Param('id') id: string, @Body() updateMenuDto: UpdateMenuDto):该方法从请求路径中提取 id 参数,从请求体中提取更新数据 updateMenuDto
  • this.menusService.update(+id, updateMenuDto):调用 MenusServiceupdate 方法,更新指定 id 的菜单。

有了这段代码,掌柜的可以修改菜品信息了:

PATCH请求.png

3.6 @Delete(':id')

@Delete(':id')
remove(@Param('id') id: string) {
  return this.menusService.remove(+id);
}
  • @Delete(':id'):这个装饰器标识此方法处理 DELETE 请求,通常用于删除资源。
  • remove(@Param('id') id: string):从请求路径中提取 id 参数。
  • this.menusService.remove(+id):调用 MenusServiceremove 方法,删除指定 id 的菜单。

有了这段代码,掌柜的可以删除菜品信息了:

DELETE请求.png

至此,可以说前言部分的 API 接口都已经完成了。

3.7 @Query()

通过 3.3 的代码就可以查看所有的菜品,但是有时候希望通过名称 name 和类型 category 去过滤菜单,这就要用到查询字符串的装饰器 @Query ,假设用户可以通过以下接口拿到过滤菜单:

  • GET /menus/search?name=beef&category=Chinese
// 新增的查询方法
@Get('search')
search(@Query('name') name: string, @Query('category') category: string) {
  return this.menusService.search({ name, category });
}

@Get('search') 装饰器将会匹配以 /menus/search 开头的路由。

⚠️ 注意:放在动态路由前面,防止被优先捕获。

对应的 menus.service.ts 中的 search 方法修改如下:

@Injectable()
export class MenusService {
  // 模拟的菜单数据
  private menus = [
    {
      id: 1,
      name: 'beef',
      category: 'Chinese',
    },
    { id: 2, name: 'pasta', category: 'Italian' },
  ];

  search(filters: { name?: string; category?: string }) {
    return this.menus.filter(menu => 
      (!filters.name || menu.name.includes(filters.name)) &&
      (!filters.category || menu.category === filters.category)
    );
  }

  // 其他方法...
}

这样就可以分别从查询字符串中拿到 namecategory 的值,并分别赋值给对应参数,再调用 MenusServicesearch 方法,找到指定 namecategory 范围下的数据了。

现在,客人可以筛选菜品了:

search条件查询.png

search条件查询2.png

4. 总结

这篇博客介绍了如何快速生成一个资源,以及控制器相关的内容。结合装饰器,控制器类能够负责处理与菜单相关的 HTTP 请求。它利用 NestJS 提供的装饰器来标识每个方法处理的请求类型(GET、POST、PATCH、DELETE)以及路径参数。其他的各种装饰器将会在以后用到时才会出现。

业务逻辑部分通过依赖注入的方式委托给 MenusService 服务类来处理。这样使得控制器更加简洁,专注于路由处理和请求解析,而将具体的业务逻辑交由服务类负责。下一篇将讲解服务类的相关内容。