Skip to content

控制器与路由:AppController ​

学习目标 ​

  • 理解 Controller 在 MVC 分层中的职责
  • 掌握 @Controller() 与 @Get() 装饰器如何定义路由
  • 理解构造函数注入的写法与原理
  • 学会扩展更多路由(路径、参数、请求方法)

涉及核心文件 ​

  • src/app.controller.ts
  • src/app.service.ts(被依赖方,第 5 章详讲)

一、完整代码与逐行讲解 ​

ts
import { Controller, Get } from "@nestjs/common";
import { AppService } from "./app.service.js";

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

第 1 行:从 @nestjs/common 导入两个装饰器。Nest 的 API 几乎全部是装饰器形式。

第 4 行:@Controller()——把这个类标记为控制器。括号里可以传路径前缀:

ts
@Controller()          // 无前缀,路由就是方法上定义的路径
@Controller('users')   // 前缀 /users,方法上的路径拼在其后

第 6 行:构造函数注入(本行是 Nest 最精髓的语法)

ts
constructor(private readonly appService: AppService) {}

一行代码同时完成三件事:

  1. TS 参数属性语法:private readonly appService: AppService 等价于声明成员变量 private readonly appService: AppService; 并在构造函数里 this.appService = appService;——这是 TypeScript 独有的简写;
  2. 声明依赖:构造签名告诉 Nest"我要一个 AppService 实例";
  3. 触发 DI:由于 tsconfig.json 开了 emitDecoratorMetadata,编译时会把参数类型(AppService)作为元数据发射出来。Nest 实例化 AppController 时读取该元数据,从 DI 容器取出 AppService 实例传入——你不用 new 任何东西。

为什么写 readonly?因为依赖在构造时注入后不该被替换,这是防御性的好习惯。

第 8-9 行:@Get() 路由方法

ts
@Get()
getHello(): string {
  return this.appService.getHello();
}
  • @Get() 把 getHello 注册为 GET / 的处理函数(无参数 = 根路径;@Get('info') 则是 GET /info)。
  • 方法体只做一件事:委托给 service。控制器保持"薄",业务逻辑都在服务层——这是 Nest 强调的分层。
  • 返回值即响应体:return 'Hello World!' → Nest 自动序列化并以 200 返回。返回对象会自动 JSON 化;返回 Promise/Observable 也支持。

二、关键概念:路由是如何被注册的 ​

装饰器本身不改变方法行为,它只是记录元数据。启动时:

Nest 扫描 AppController 原型
  → 发现 getHello 方法上有 @Get() 留下的元数据(path='/', method='GET')
  → 结合 @Controller() 的前缀('')
  → 向 Express 注册:app.get('/', (req, res) => controller.getHello() 的包装)

所以访问 GET / 时,本质是 Express 调用了包装过的 getHello()。

三、关键概念:常用 HTTP 装饰器速查 ​

装饰器作用示例
@Get() / @Post() / @Put() / @Delete() / @Patch()绑定 HTTP 方法@Post('create')
@Param('id')取路径参数GET /users/42 → id='42'
@Query('page')取查询参数?page=2
@Body()取请求体POST 的 JSON body
@Headers('x-token')取请求头
@HttpCode(204)自定义状态码

四、动手实践 ​

实践 1:加一个带前缀和参数的路由 ​

把 app.controller.ts 改为:

ts
import { Controller, Get, Param, Query } from "@nestjs/common";
import { AppService } from "./app.service.js";

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }

  @Get("hello/:name")
  greet(@Param("name") name: string, @Query("lang") lang?: string): string {
    return lang === "en" ? `Hello, ${name}!` : `你好,${name}!`;
  }
}

启动后验证:

bash
curl http://localhost:3000/hello/张三
# → 你好,张三!
curl "http://localhost:3000/hello/张三?lang=en"
# → Hello, 张三!

实践 2:体会返回值序列化 ​

新增一个返回对象的方法:

ts
@Get('info')
getInfo() {
  return { name: 'nest-typescript-starter', time: new Date().toISOString() };
}

访问 /info,观察响应自动变成了 JSON,且 Content-Type: application/json——Nest 根据返回值类型自动处理序列化。

实践 3:制造 404 ​

访问一个未定义的路由如 curl http://localhost:3000/nothing,观察 Nest 内置的 404 JSON 响应结构:

json
{ "message": "Cannot GET /nothing", "error": "Not Found", "statusCode": 404 }

这说明框架默认接管了"路由未匹配"的情况。

本章小结 ​

  • Controller 职责单一:定义路由、解析请求、调用 Service、返回结果。
  • @Controller(前缀) + @Get(路径) 拼出完整路由;返回值自动序列化为响应。
  • constructor(private readonly appService: AppService) 一行完成属性声明与依赖注入声明。
  • 控制器"薄"、服务"厚"是 Nest 的分层纪律。下一章进入服务层,看 @Injectable 与 DI 容器的完整机制。