1. 前言
同福客栈应该有一个菜单供客人点餐,每一道菜都有自己的信息(比如配料是什么、价格多少),配料和价格可以调整,可以不提供这道菜。
简而言之,菜单就是一个资源,RESTful 风格的 API 接口如下:
- GET /menus 获取所有菜品
- GET /menus/:id 获取某个菜品详情
- POST /menus 添加新的菜品
- PATCH /menus/:id 修改菜品详情
- DELETE /menus/:id 删除某个菜品
欢迎加入技术交流群。
- NestJS 🧑🍳 厨子必修课(一):后端的本质
- NestJS 🧑🍳 厨子必修课(二):项目创建
- NestJS 🧑🍳 厨子必修课(三):控制器
- NestJS 🧑🍳 厨子必修课(四):服务类
- NestJS 🧑🍳 厨子必修课(五):Prisma 集成(上)
- NestJS 🧑🍳 厨子必修课(六):Prisma 集成(下)
- NestJS 🧑🍳 厨子必修课(七):管道
- NestJS 🧑🍳 厨子必修课(八):异常过滤器
2. 快速生成 menus 资源
通过以下命令快速生成一个有关 menus 资源的控制器文件:
$ nest g resource menus
执行后会在 src 目录下生成一个有关 menus 资源的文件夹:
可以看到这组资源的构成与外层的 app 是类似的,都由 controller、service、module 文件构成。
先前说过“约定大于配置”,因此可以预见的是 controller 文件里应该写了 menus 相关的路由匹配以及对应执行的 service 方法,而 service 文件中包含了业务处理逻辑和具体响应给前端的内容,module 则负责如何组织它们。
3. 控制器
打开 menus.controller.ts 文件可以看到已经自动生成了相关代码,整体结构如下:
// 引入相关依赖...
@Controller('menus')
export class MenusController {
// 相关代码...
}
在 Nest 中使用了大量的装饰器语法,可以理解为**“武器赋魔”,使之具备某种能力**。
@Controller
用于将类 MenusController
标记为控制器,控制器是用于处理传入的 HTTP 请求的类。它定义了应用程序的路由,并根据不同的 HTTP 方法(如 GET
、POST
、PUT
、DELETE
等)来处理请求。
@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
依赖 MenusService
,MenusService
为 MenusController
的依赖项。
除了能够匹配到路由,下一步是要处理对应的 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
将负责处理实际的创建逻辑。
有了这段代码,掌柜的可以添加新的菜品了:
3.3 @Get()
@Get()
findAll() {
return this.menusService.findAll();
}
@Get()
:这个装饰器标识此方法处理 GET 请求。通常用于获取资源的列表。findAll()
:直接调用MenusService
的findAll
方法来获取所有菜单。
有了这段代码,客人们可以查看所有的菜品了:
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)
:调用MenusService
的findOne
方法,传入的id
参数前加上+
表示将字符串id
转换为数字。
有了这段代码,客人们可以查看某一道菜品的详情了:
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)
:调用MenusService
的update
方法,更新指定id
的菜单。
有了这段代码,掌柜的可以修改菜品信息了:
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)
:调用MenusService
的remove
方法,删除指定id
的菜单。
有了这段代码,掌柜的可以删除菜品信息了:
至此,可以说前言部分的 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)
);
}
// 其他方法...
}
这样就可以分别从查询字符串中拿到 name
和 category
的值,并分别赋值给对应参数,再调用 MenusService
的 search
方法,找到指定 name
和 category
范围下的数据了。
现在,客人可以筛选菜品了:
4. 总结
这篇博客介绍了如何快速生成一个资源,以及控制器相关的内容。结合装饰器,控制器类能够负责处理与菜单相关的 HTTP 请求。它利用 NestJS 提供的装饰器来标识每个方法处理的请求类型(GET、POST、PATCH、DELETE)以及路径参数。其他的各种装饰器将会在以后用到时才会出现。
业务逻辑部分通过依赖注入的方式委托给 MenusService
服务类来处理。这样使得控制器更加简洁,专注于路由处理和请求解析,而将具体的业务逻辑交由服务类负责。下一篇将讲解服务类的相关内容。