如果你刚接触这个框架,可能会被一堆新名词吓到:控制器、提供者、模块、装饰器......听起来像是一群外星人在开会。别怕,今天我们就用最接地气的方式,把 控制器 和 装饰器 这两兄弟彻底搞明白。学完这篇,你就能自己写一个简单的 API 了,而且会觉得:哎,原来这么简单 💡!
## 🎯从一个"混乱的厨房"说起
想象你开了一家餐厅,刚开始生意小,你自己既当服务员又当厨师。客人来了,你递菜单(接收请求),然后冲到后厨炒菜(处理业务),最后端菜上桌(返回响应)。一切看起来还行。
突然餐厅火了,客人爆满。你一个人根本忙不过来:一会儿要记菜单,一会儿要炒菜,一会儿还要收钱,厨房乱成一锅粥。这时候你会怎么办?当然是分工啊!
你雇了专门的服务员,负责接待客人、记菜单、上菜。后厨请了专业厨师,只管按照菜单做菜。客人多了也不怕,服务员分工明确:有的专门负责大厅,有的负责包间,有的专门上菜。
在 NestJS 里,控制器(Controller) 就是那个"服务员"。它的任务就是:
- 接客:接收来自客户端(浏览器、手机 App)的 HTTP 请求。
- 喊后厨:把请求转交给真正的业务逻辑处理层(也就是服务,Service),让专业的人干专业的事。
- 端菜:把处理结果包装成 HTTP 响应,发回给客户端。
而服务员手里拿的菜单 ,就是路由------它告诉服务员,什么样的客人(请求路径和方法)该找谁。
❓那装饰器又是什么鬼?
好了,现在你有了服务员(控制器),但你需要告诉服务员:"嘿,这位客人(GET /cats)要查所有猫咪,你去找小张(findAll 方法);那位客人(POST /cats)要新增一只猫,你去找小李(create 方法)。"
怎么告诉服务员呢?你不可能给每个服务员发一本厚厚的操作手册吧?那样太笨了。
聪明的做法是:在每个人身上贴标签。比如在小张的脑门上贴一张便利贴,上面写着"处理 GET /cats 请求"。小李的脑门上贴"处理 POST /cats 请求"。这样服务员一看标签,就知道谁该干什么。
在 NestJS 里,装饰器(Decorator) 就是这些便利贴。它们贴在类上、方法上、参数上,告诉框架:这个玩意儿有特殊用途,你要这样对待它。
装饰器以 @ 开头,后面跟着一个名字,比如 @Controller、@Get、@Param。它们就像是给代码贴的标签,框架在启动时会扫描这些标签,自动帮你建立起路由映射。
如果你想知道如何创建自己的装饰器,就留言告诉小编,文章第一时间奉上😄😄~~
🚶来吧,手写一个最简单的控制器(示例参考官网)
别光听理论,我们直接动手写代码。假设我们要写一个"猫咪管理"的 API,提供查询所有猫咪和查询单只猫咪的功能。
第一步:创建控制器文件
在 src 目录下新建一个文件 cats.controller.ts。名字随便起,但一般习惯用 xxx.controller.ts。
第二步:写一个空类,贴上 @Controller 标签
typescript
// cats.controller.ts
import { Controller } from '@nestjs/common';
@Controller('cats') // ← 贴标签:这个类是控制器,负责处理所有 /cats 开头的请求
export class CatsController {
// 待会儿往里加方法
}
这里的 @Controller('cats') 就是给这个类贴的标签,告诉 Nest:这个服务员负责的区域是 /cats 路径。也就是说,所有以 /cats 开头的请求都会先送到这个类里来。
第三步:添加方法,贴上 HTTP 方法标签
typescript
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get() // ← 贴标签:这个方法要处理 GET 请求
findAll() {
return ['加菲猫', '汤姆猫', '哆啦A梦']; // 返回数据,框架会自动转成 JSON
}
@Get(':id') // ← 贴标签:处理 GET /cats/xxx 的请求,:id 是动态参数
findOne() {
return '这只猫的信息';
}
}
这里有两个方法:
findAll()贴了@Get(),表示它处理GET /cats的请求(注意类前缀是/cats,方法没写额外路径,所以就是/cats)。findOne()贴了@Get(':id'),:id是一个占位符,表示它可以匹配像/cats/1、/cats/abc这样的请求。等一下,我们怎么拿到那个id呢?继续往下看。
第四步:用参数装饰器拿数据
typescript
import { Controller, Get, Param } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
return ['加菲猫', '汤姆猫', '哆啦A梦'];
}
@Get(':id')
findOne(@Param('id') id: string) { // ← @Param('id') 把路由参数 id 的值取出来
return `你查询的是第 ${id} 号猫咪`;
}
}
看,@Param('id') 这个便利贴贴在 id 参数上,告诉框架:把路由里的 id 参数的值赋给这个变量。如果你访问 GET /cats/123,就会返回"你查询的是第 123 号猫咪"。
第五步:别忘了把控制器注册到模块里
控制器写好了,但 Nest 还不知道它的存在。你需要把它添加到某个模块的 controllers 数组里。通常我们会在根模块 app.module.ts 里注册:
typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
@Module({
controllers: [CatsController], // ← 把控制器加进来
})
export class AppModule {}
这样 Nest 启动时就会加载这个控制器,并建立好路由。
再来看几个常用的便利贴(装饰器)
刚才我们用了 @Controller、@Get、@Param。其实 Nest 还提供了很多其他便利贴,让你能轻松拿到请求里的各种数据。我们快速过一遍,都很直观。
处理 POST 请求:@Post 和 @Body
如果你想新增一只猫咪,客户端会发一个 POST 请求,并在请求体里带上猫咪的数据。你可以这样写:
typescript
import { Controller, Post, Body } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create(@Body() body: any) { // @Body() 拿到整个请求体
console.log(body); // { name: '加菲', age: 5 }
return '猫咪创建成功';
}
}
如果只想取请求体里的某个字段,可以指定参数名:
typescript
@Post()
create(@Body('name') name: string) { // 只取 body 里的 name 字段
return `创建猫咪 ${name}`;
}
拿查询参数:@Query
比如请求是 GET /cats?age=2&color=orange,你想拿到 age 和 color:
typescript
@Get()
findAll(@Query('age') age: string, @Query('color') color: string) {
return `年龄:${age},颜色:${color}`;
}
或者一次性拿到整个 query 对象:
typescript
@Get()
findAll(@Query() query: any) {
return query; // { age: '2', color: 'orange' }
}
拿请求头:@Headers
typescript
@Get()
findAll(@Headers('authorization') auth: string) {
return `你的 token 是:${auth}`;
}
拿原始的请求和响应对象:@Req 和 @Res
虽然不常用,但如果你非要自己操作底层的请求/响应对象(比如用 Express 的 res 直接发响应),可以用 @Req 和 @Res:
typescript
import { Req, Res } from '@nestjs/common';
import { Request, Response } from 'express';
@Get()
findAll(@Req() req: Request, @Res() res: Response) {
// 自己控制响应
res.status(200).json({ data: '...' });
}
不过要注意,一旦用了 @Res,你就得自己负责发送响应,否则请求会卡住。如果想在设置一些东西(比如 cookie)后仍然让框架帮忙发响应,可以加 { passthrough: true }:
typescript
@Get()
findAll(@Res({ passthrough: true }) res: Response) {
res.setHeader('X-Custom', 'Hello');
return '这个返回值会由框架处理'; // 没问题,框架依然会发这个响应
}
修改响应状态码:@HttpCode
默认情况下,GET 请求返回 200,POST 返回 201。如果你想改,可以贴 @HttpCode:
typescript
@Post()
@HttpCode(204) // 返回 204 No Content
create() {
return '创建成功,但不返回内容';
}
加个响应头:@Header
typescript
@Get()
@Header('Cache-Control', 'no-store')
findAll() {
return ['加菲猫', '汤姆猫'];
}
重定向:@Redirect
typescript
@Get('old-path')
@Redirect('https://example.com/new-path', 301)
goToNew() {
// 直接重定向
}
还可以动态决定跳哪:
typescript
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version: string) {
if (version === '5') {
return { url: 'https://docs.nestjs.com/v5/' }; // 返回这个会覆盖上面的装饰器参数
}
}
来一个完整的例子:猫咪 CRUD
我们组合一下学过的知识,写一个简单的猫咪增删改查控制器。别怕,代码不长,每一步都有注释。
typescript
// cats.controller.ts
import { Controller, Get, Post, Put, Delete, Param, Body, Query, HttpCode } from '@nestjs/common';
// 先定义一个简单的数据类,用来接收请求体
class CreateCatDto {
name: string;
age: number;
breed: string;
}
@Controller('cats')
export class CatsController {
// 假装有一个数据库,实际上用个数组存着
private cats = [
{ id: 1, name: '加菲', age: 5, breed: '异国短毛' },
{ id: 2, name: '汤姆', age: 3, breed: '英短' },
];
private nextId = 3;
// 1. GET /cats - 获取所有猫咪,支持分页查询 ?limit=10
@Get()
findAll(@Query('limit') limit: number) {
const result = limit ? this.cats.slice(0, limit) : this.cats;
return result;
}
// 2. GET /cats/:id - 获取单只猫咪
@Get(':id')
findOne(@Param('id') id: string) {
const cat = this.cats.find(c => c.id === Number(id));
if (!cat) {
return { message: '猫咪没找到' };
}
return cat;
}
// 3. POST /cats - 创建新猫咪
@Post()
@HttpCode(201) // 其实默认就是201,写出来让你看看怎么用
create(@Body() createCatDto: CreateCatDto) {
const newCat = { id: this.nextId++, ...createCatDto };
this.cats.push(newCat);
return newCat;
}
// 4. PUT /cats/:id - 更新猫咪
@Put(':id')
update(@Param('id') id: string, @Body() updateCatDto: CreateCatDto) {
const index = this.cats.findIndex(c => c.id === Number(id));
if (index === -1) {
return { message: '猫咪不存在' };
}
this.cats[index] = { id: Number(id), ...updateCatDto };
return this.cats[index];
}
// 5. DELETE /cats/:id - 删除猫咪
@Delete(':id')
remove(@Param('id') id: string) {
const index = this.cats.findIndex(c => c.id === Number(id));
if (index === -1) {
return { message: '猫咪不存在' };
}
const removed = this.cats.splice(index, 1);
return removed[0];
}
}
然后把控制器加到 app.module.ts 里:
typescript
// app.module.ts
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
@Module({
controllers: [CatsController],
})
export class AppModule {}
启动应用(npm run start:dev),你就可以用 Postman 或者浏览器测试这些接口了。是不是很简单?
总结:控制器 + 装饰器 = 你的得力助手
现在我们来捋一捋:
- 控制器 就是服务员,负责接待请求、分派任务、返回响应。
- 装饰器 就是便利贴,贴在类、方法、参数上,告诉框架它们的作用。
- 你只需要用这些便利贴声明"这个类处理 /cats 路径"、"这个方法处理 GET 请求"、"这个参数从路由里拿 id",剩下的路由解析、参数提取、响应转换......框架全帮你干了。
初学者最容易犯的错误是忘记在模块里注册控制器,或者装饰器写错位置(比如把 @Get 贴在类上)。别担心,写错几次就有经验了。
最后,送给你一句话:把 NestJS 想象成一个超级聪明的餐厅经理,你只需要贴好标签,它就能让一切井井有条。下一篇我们会聊聊"服务(Service)"这个后厨大厨,敬请期待!