覆盖文档:Controllers, Versioning 前置知识:第一课(NestJS 基础概念、项目创建、模块系统) 源码重点:
packages/common/decorators/http/request-mapping.decorator.ts,packages/common/decorators/core/controller.decorator.ts,packages/core/router/routes-resolver.ts,packages/core/router/router-explorer.ts,packages/core/router/paths-explorer.ts
一、路由与请求处理
[基础] 本节面向初学者,掌握 Controller 的核心用法。
1.1 什么是 Controller
Controller 是 NestJS 中处理 HTTP 请求的入口。每个 Controller 负责接收特定路由的请求,执行相应的业务逻辑,并返回响应。
客户端请求 → 路由匹配 → Controller 方法 → 返回响应
↓
调用 Service 处理业务
1.2 @Controller() 路径前缀
@Controller() 装饰器将一个类标记为控制器,可选参数指定路由前缀:
import { Controller, Get } from '@nestjs/common';
@Controller('cats') // 路由前缀:/cats
export class CatsController {
@Get() // 完整路由:GET /cats
findAll(): string {
return 'This action returns all cats';
}
@Get('breed') // 完整路由:GET /cats/breed
findBreed(): string {
return 'This action returns all cat breeds';
}
}
路由组合规则:全局前缀 + 模块路径 + Controller前缀 + 方法路径
1.3 HTTP 方法装饰器
NestJS 为每种 HTTP 方法提供了对应的装饰器:
import {
Controller, Get, Post, Put, Delete, Patch, Options, Head, All,
} from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get() // GET /cats
findAll() { return []; }
@Get(':id') // GET /cats/:id
findOne() { return {}; }
@Post() // POST /cats
create() { return 'created'; }
@Put(':id') // PUT /cats/:id
update() { return 'updated'; }
@Delete(':id') // DELETE /cats/:id
remove() { return 'removed'; }
@Patch(':id') // PATCH /cats/:id
partialUpdate() { return 'patched'; }
@Options() // OPTIONS /cats
options() { return; }
@Head() // HEAD /cats
head() { return; }
@All('any') // 匹配所有 HTTP 方法 → /cats/any
handleAll() { return 'any method'; }
}
| 装饰器 | HTTP 方法 | 语义 |
|---|---|---|
@Get() | GET | 查询资源 |
@Post() | POST | 创建资源 |
@Put() | PUT | 全量替换资源 |
@Patch() | PATCH | 部分更新资源 |
@Delete() | DELETE | 删除资源 |
@Options() | OPTIONS | 预检请求(CORS) |
@Head() | HEAD | 获取响应头(无 body) |
@All() | ALL | 匹配所有方法 |
1.4 请求参数提取
NestJS 提供了丰富的参数装饰器,用于从请求对象中提取数据:
import {
Controller, Get, Post, Param, Query, Body, Headers, Ip, Req, Res, Session, HostParam,
} from '@nestjs/common';
import { Request, Response } from 'express';
import { CreateCatDto } from './dto/create-cat.dto';
@Controller('cats')
export class CatsController {
// 路由参数
@Get(':id')
findOne(@Param('id') id: string): string {
return `Cat #${id}`;
}
// 多个路由参数
@Get(':userId/cats/:catId')
findUserCat(
@Param('userId') userId: string,
@Param('catId') catId: string,
): string {
return `User ${userId}, Cat ${catId}`;
}
// 查询参数
@Get()
findAll(
@Query('page') page: number = 1,
@Query('limit') limit: number = 10,
): string {
return `Page ${page}, Limit ${limit}`;
}
// 请求体
@Post()
create(@Body() createCatDto: CreateCatDto): string {
return `Created cat: ${createCatDto.name}`;
}
// 请求体的单个字段
@Post('name')
createName(@Body('name') name: string): string {
return `Name: ${name}`;
}
// 请求头
@Get('auth')
getAuth(@Headers('authorization') auth: string): string {
return `Auth: ${auth}`;
}
// IP 地址
@Get('ip')
getIp(@Ip() ip: string): string {
return `IP: ${ip}`;
}
// 原始请求对象(不推荐,降低平台可移植性)
@Get('raw')
getRaw(@Req() req: Request): string {
return `Method: ${req.method}, URL: ${req.url}`;
}
}
1.5 装饰器到请求对象映射表
| 装饰器 | 对应 Express 对象 |
|---|---|
@Request() / @Req() | req |
@Response() / @Res() | res |
@Next() | next |
@Session() | req.session |
@Param(key?) | req.params / req.params[key] |
@Body(key?) | req.body / req.body[key] |
@Query(key?) | req.query / req.query[key] |
@Headers(name?) | req.headers / req.headers[name] |
@Ip() | req.ip |
@HostParam(key?) | req.hosts / req.hosts[key] |
注意:使用
@Res()后,NestJS 不会自动发送响应,你必须手动调用res.json()或res.send()。如果既想用res又想保持自动响应,请使用@Res({ passthrough: true })(详见 2.5 节)。
1.6 DTO(Data Transfer Object)
DTO 用于定义和验证请求体的数据结构。NestJS 推荐使用 class 而非 interface 定义 DTO,因为 class 在运行时保留,可配合 Pipe 做校验:
// dto/create-cat.dto.ts
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}
// dto/update-cat.dto.ts — 所有字段可选(部分更新)
export class UpdateCatDto {
readonly name?: string;
readonly age?: number;
readonly breed?: string;
}
配合 class-validator 实现自动验证(后续课程详解 Pipe):
import { IsString, IsInt, Min, Max } from 'class-validator';
export class CreateCatDto {
@IsString()
readonly name: string;
@IsInt()
@Min(0)
@Max(30)
readonly age: number;
@IsString()
readonly breed: string;
}
为什么用 class 不用 interface? TypeScript 的 interface 在编译后会被擦除,运行时不存在。而 class 编译后仍然是一个构造函数,
class-validator和class-transformer等库需要在运行时读取元数据,所以必须使用 class。
1.7 响应状态码
默认情况下,POST 请求返回 201,其他方法返回 200。使用 @HttpCode() 修改:
import { Controller, Post, HttpCode, HttpStatus } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
@HttpCode(HttpStatus.NO_CONTENT) // 204 No Content
remove(): void {
// 无返回体
}
@Post('batch')
@HttpCode(HttpStatus.ACCEPTED) // 202 Accepted(异步处理)
batchCreate(): string {
return 'Batch job started';
}
}
常用状态码枚举(HttpStatus):
| 枚举值 | 状态码 | 含义 |
|---|---|---|
OK | 200 | 成功 |
CREATED | 201 | 已创建 |
ACCEPTED | 202 | 已接受(异步处理) |
NO_CONTENT | 204 | 无内容 |
BAD_REQUEST | 400 | 客户端错误 |
UNAUTHORIZED | 401 | 未授权 |
FORBIDDEN | 403 | 禁止访问 |
NOT_FOUND | 404 | 未找到 |
1.8 响应头
使用 @Header() 设置自定义响应头:
import { Controller, Get, Header } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
@Header('Cache-Control', 'no-store')
@Header('X-Custom-Header', 'my-value')
findAll(): string {
return 'This action returns all cats';
}
}
1.9 重定向
使用 @Redirect() 实现路由重定向:
import { Controller, Get, Redirect, Query } from '@nestjs/common';
@Controller('cats')
export class CatsController {
// 静态重定向
@Get('old')
@Redirect('https://nestjs.com', 301) // 301 永久重定向
handleOld() {
// 不需要返回值
}
// 动态重定向 — 返回值会覆盖 @Redirect 参数
@Get('docs')
@Redirect('https://docs.nestjs.com', 302)
getDocs(@Query('version') version: string) {
if (version === '5') {
return { url: 'https://docs.nestjs.com/v5/', statusCode: 302 };
}
// 不返回值则使用装饰器中的默认地址
}
}
返回对象的结构:{ url: string, statusCode: number }
二、高级路由技巧
[中阶] 本节面向有一定经验的开发者,掌握复杂路由场景。
2.1 路由通配符
NestJS 支持基于模式的路由匹配(底层使用 path-to-regexp):
@Controller('cats')
export class CatsController {
// 匹配 /cats/abcd, /cats/abcd/efgh, /cats/abcd/123 等
@Get('abcd/*splat')
findWild(): string {
return 'This route uses a wildcard';
}
// 正则约束路由参数
@Get(':id(\\d+)') // 只匹配数字
findOne(@Param('id') id: string): string {
return `Cat #${id}`;
}
}
注意:Express 和 Fastify 对通配符的语法支持略有不同。上面的
*splat语法适用于path-to-regexpv8+(NestJS 11)。在旧版本中,通配符使用*或(.*)语法。
2.2 路由参数排序
静态路由必须定义在动态(参数化)路由之前,否则静态路由会被参数化路由"吞掉":
@Controller('cats')
export class CatsController {
// 静态路由 — 必须放在前面
@Get('breed')
findBreed(): string {
return 'All breeds';
}
// 参数化路由 — 放在后面
@Get(':id')
findOne(@Param('id') id: string): string {
return `Cat ${id}`;
}
}
// 如果 @Get(':id') 放在 @Get('breed') 前面,
// 访问 GET /cats/breed 会匹配到 findOne(),id='breed'
2.3 子域名路由
@Controller() 支持通过 host 选项做子域名路由:
// 匹配 admin.example.com 的请求
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index(): string {
return 'Admin page';
}
}
// 动态子域名 — 提取子域名参数
@Controller({ host: ':account.example.com' })
export class AccountController {
@Get()
getInfo(@HostParam('account') account: string): string {
return `Account: ${account}`;
}
}
注意:子域名路由要求底层 HTTP 适配器支持。Fastify 不支持嵌套路由器,因此使用子域名路由时推荐 Express 平台。
2.4 异步控制器
Controller 方法天然支持 async/await:
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
// async/await 方式
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
// RxJS Observable 方式 — NestJS 会自动订阅并发送最终值
@Get('observable')
findAllObservable(): Observable<Cat[]> {
return this.catsService.findAllObservable();
}
}
两种方式完全等价,NestJS 内部统一处理。选择标准:
- 团队熟悉 Promise → 用
async/await - 需要流式/响应式操作(如 WebSocket、SSE) → 用
Observable
2.5 Library-Specific 模式
当需要直接操作底层响应对象(如设置 Cookie、流式传输文件)时,可以注入 @Res():
import { Controller, Get, Res } from '@nestjs/common';
import { Response } from 'express';
@Controller('cats')
export class CatsController {
// 完全接管响应 — 必须手动发送响应
@Get('download')
download(@Res() res: Response) {
res.setHeader('Content-Type', 'application/octet-stream');
res.download('/path/to/file');
// NestJS 不再处理返回值
}
// passthrough 模式 — 既能用 res 设置头,又保持自动响应
@Get('with-cookie')
withCookie(@Res({ passthrough: true }) res: Response): string {
res.cookie('token', 'abc123');
return 'Cookie has been set'; // NestJS 自动发送这个返回值
}
}
| 模式 | @Res() | @Res({ passthrough: true }) |
|---|---|---|
| 响应发送 | 手动 res.send() | 自动(框架处理) |
| 可设置响应头 | 是 | 是 |
| 可设置状态码 | res.status() | @HttpCode() 或 res.status() |
| 拦截器生效 | 否 | 是 |
| 推荐场景 | 文件下载、流式传输 | 设置 Cookie、自定义头 |
2.6 复杂查询参数解析
@Controller('cats')
export class CatsController {
@Get()
findAll(
@Query('page') page: number = 1,
@Query('limit') limit: number = 20,
@Query('sort') sort: string = 'createdAt',
@Query('order') order: 'asc' | 'desc' = 'desc',
@Query('breed') breed?: string,
): string {
// GET /cats?page=2&limit=10&sort=name&order=asc&breed=persian
return `Page ${page}, Limit ${limit}, Sort ${sort} ${order}`;
}
// 获取所有查询参数为一个对象
@Get('search')
search(@Query() query: Record<string, any>): object {
return query;
}
}
注意:
@Query()获取的值默认为string类型。如果需要自动类型转换,需使用ValidationPipe配合class-transformer(后续课程 Pipe 详解)。
2.7 单例与请求作用域
Controller 默认是单例的(整个应用生命周期只有一个实例),所有请求共享同一实例:
// 默认:单例(Singleton)
@Controller('cats')
export class CatsController {
// 注意:不要在 Controller 中存储请求相关的状态!
// private currentUser; ← 错误!多个并发请求会互相覆盖
}
需要请求级隔离时,使用 Scope.REQUEST:
import { Controller, Scope } from '@nestjs/common';
// 每个请求创建一个新实例
@Controller({ path: 'cats', scope: Scope.REQUEST })
export class CatsController {
// 现在可以安全存储请求级状态
// 但性能会下降,因为每个请求都要实例化
}
最佳实践:99% 的情况使用默认的单例模式。只有需要请求级状态(如多租户场景、请求级缓存)时才使用
Scope.REQUEST。
三、API 版本控制
[高阶] 本节面向需要管理 API 版本迭代的开发者。
3.1 为什么需要版本控制
当 API 的行为需要变更而旧客户端仍在使用时,版本控制允许同一接口提供多个版本并行运行,确保向后兼容。
3.2 四种版本控制策略
在 main.ts 中启用版本控制:
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 策略一:URI 版本控制(最常用)
// 路由:GET /v1/cats, GET /v2/cats
app.enableVersioning({
type: VersioningType.URI,
});
// 策略二:Header 版本控制
// 请求头:Custom-Header: 1
app.enableVersioning({
type: VersioningType.HEADER,
header: 'Custom-Header',
});
// 策略三:Media Type 版本控制
// Accept: application/json;v=1
app.enableVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'v=',
});
// 策略四:Custom 版本控制
// 自定义版本提取逻辑
app.enableVersioning({
type: VersioningType.CUSTOM,
extractor: (request: Request) => {
// 从任意位置提取版本号
return request.headers['x-api-version'] || '1';
},
});
await app.listen(3000);
}
bootstrap();
| 策略 | 客户端传参方式 | 示例 | 适用场景 |
|---|---|---|---|
| URI | URL 路径 | GET /v1/cats | 公共 API,最直观 |
| Header | 请求头 | X-API-Version: 1 | 内部 API,URL 保持简洁 |
| Media Type | Accept 头 | Accept: app/json;v=1 | 严格遵循 REST 标准 |
| Custom | 自定义逻辑 | 任意 | 特殊需求 |
3.3 Controller 级版本
整个 Controller 绑定到一个版本:
@Controller({
path: 'cats',
version: '1', // 所有路由属于 v1
})
export class CatsControllerV1 {
@Get() // GET /v1/cats
findAll(): string {
return 'v1: all cats';
}
}
@Controller({
path: 'cats',
version: '2', // 所有路由属于 v2
})
export class CatsControllerV2 {
@Get() // GET /v2/cats
findAll(): string {
return 'v2: all cats with pagination';
}
}
3.4 路由级版本
同一个 Controller 内不同方法属于不同版本,使用 @Version() 装饰器:
import { Controller, Get, Version } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Version('1')
@Get() // GET /v1/cats
findAllV1(): string {
return 'v1: all cats';
}
@Version('2')
@Get() // GET /v2/cats
findAllV2(): string {
return 'v2: all cats with enhanced response';
}
// 同时响应多个版本
@Version(['1', '2'])
@Get('count') // GET /v1/cats/count 和 GET /v2/cats/count
count(): number {
return 42;
}
}
3.5 VERSION_NEUTRAL — 版本无关路由
某些路由(如健康检查)不应受版本控制影响:
import { Controller, Get, VERSION_NEUTRAL } from '@nestjs/common';
@Controller({
path: 'health',
version: VERSION_NEUTRAL, // 不带版本前缀
})
export class HealthController {
@Get() // GET /health(无论版本策略)
check(): string {
return 'OK';
}
}
也可以在默认版本中设置全局默认:
app.enableVersioning({
type: VersioningType.URI,
defaultVersion: '1', // 未标注版本的路由默认为 v1
// 或
defaultVersion: VERSION_NEUTRAL, // 未标注版本的路由不受版本控制
// 或
defaultVersion: ['1', '2'], // 未标注版本的路由同时响应 v1 和 v2
});
3.6 中间件版本控制
中间件也可以按版本过滤:
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
@Module({})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LoggerMiddleware)
.forRoutes({
path: 'cats',
method: RequestMethod.GET,
version: '2', // 仅对 v2 的 GET /cats 生效
});
}
}
四、源码:路由注册机制
[资深] 本节面向希望深入理解框架内部机制的读者。
4.1 @Controller() 装饰器源码
文件位置:packages/common/decorators/core/controller.decorator.ts
export function Controller(
prefixOrOptions?: string | string[] | ControllerOptions,
): ClassDecorator {
const defaultPath = '/';
const [path, host, scopeOptions, versionOptions] = isUndefined(
prefixOrOptions,
)
? [defaultPath, undefined, undefined, undefined]
: isString(prefixOrOptions) || Array.isArray(prefixOrOptions)
? [prefixOrOptions, undefined, undefined, undefined]
: [
prefixOrOptions.path || defaultPath,
prefixOrOptions.host,
{ scope: prefixOrOptions.scope, durable: prefixOrOptions.durable },
Array.isArray(prefixOrOptions.version)
? Array.from(new Set(prefixOrOptions.version))
: prefixOrOptions.version,
];
return (target: object) => {
Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target);
Reflect.defineMetadata(PATH_METADATA, path, target);
Reflect.defineMetadata(HOST_METADATA, host, target);
Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, scopeOptions, target);
Reflect.defineMetadata(VERSION_METADATA, versionOptions, target);
};
}
源码解读:
- 参数多态:支持三种调用方式 —
@Controller()、@Controller('cats')、@Controller({ path: 'cats', host: '...', version: '1' }) - 元数据存储:通过
Reflect.defineMetadata将信息存储在类上CONTROLLER_WATERMARK:标记为控制器(布尔值true),模块扫描器据此识别 ControllerPATH_METADATA:路由路径前缀HOST_METADATA:子域名约束SCOPE_OPTIONS_METADATA:作用域(Singleton / Request / Transient)VERSION_METADATA:API 版本号
- 版本去重:当版本是数组时,使用
new Set()去重
4.2 @Get() / @Post() 等装饰器源码
文件位置:packages/common/decorators/http/request-mapping.decorator.ts
// 底层 RequestMapping 装饰器
export const RequestMapping = (
metadata: RequestMappingMetadata = defaultMetadata,
): MethodDecorator => {
const pathMetadata = metadata[PATH_METADATA];
const path = pathMetadata && pathMetadata.length ? pathMetadata : '/';
const requestMethod = metadata[METHOD_METADATA] || RequestMethod.GET;
return (
target: object,
key: string | symbol,
descriptor: TypedPropertyDescriptor<any>,
) => {
Reflect.defineMetadata(PATH_METADATA, path, descriptor.value);
Reflect.defineMetadata(METHOD_METADATA, requestMethod, descriptor.value);
return descriptor;
};
};
// 工厂函数 — 生成各 HTTP 方法装饰器
const createMappingDecorator =
(method: RequestMethod) =>
(path?: string | string[]): MethodDecorator => {
return RequestMapping({
[PATH_METADATA]: path,
[METHOD_METADATA]: method,
});
};
// 所有 HTTP 方法装饰器都是工厂的产物
export const Get = createMappingDecorator(RequestMethod.GET);
export const Post = createMappingDecorator(RequestMethod.POST);
export const Put = createMappingDecorator(RequestMethod.PUT);
export const Delete = createMappingDecorator(RequestMethod.DELETE);
export const Patch = createMappingDecorator(RequestMethod.PATCH);
export const Options = createMappingDecorator(RequestMethod.OPTIONS);
export const Head = createMappingDecorator(RequestMethod.HEAD);
export const All = createMappingDecorator(RequestMethod.ALL);
源码解读:
- 工厂模式:
createMappingDecorator是一个高阶函数,接收 HTTP 方法,返回装饰器 - 元数据位置:注意与
@Controller不同 — 这里的元数据存储在 方法(descriptor.value)上,而不是类上 - 关键元数据:
PATH_METADATA:方法级路由路径METHOD_METADATA:HTTP 方法枚举值(RequestMethod.GET= 0,.POST= 1, ...)
4.3 路由注册流程
应用启动时,路由是如何从装饰器变成可访问的 HTTP 端点的?
NestFactory.create(AppModule)
│
├─→ DependenciesScanner.scan() 扫描所有模块,收集 Controller
│
├─→ InstanceLoader 实例化所有 Controller 和 Provider
│
└─→ RoutesResolver.resolve() 注册路由到 HTTP 引擎
│
├─→ 遍历所有模块的 controllers Map
│
├─→ RouterExplorer.extractRouterPath(metatype)
│ → 读取 @Controller 的 PATH_METADATA
│ → 得到 Controller 前缀(如 '/cats')
│
├─→ RouterExplorer.explore(instanceWrapper, ...)
│ │
│ ├─→ PathsExplorer.scanForPaths(instance)
│ │ → MetadataScanner 扫描实例的所有方法
│ │ → 对每个方法读取 PATH_METADATA 和 METHOD_METADATA
│ │ → 返回 RouteDefinition[] 数组
│ │
│ └─→ applyPathsToRouterProxy()
│ → 组合 globalPrefix + modulePath + ctrlPath + methodPath
│ → 调用 httpAdapter.get/post/put/delete(fullPath, handler)
│ → 路由正式注册到 Express/Fastify
│
└─→ 日志输出:Mapped {GET, /v1/cats} route
4.4 RoutesResolver — 顶层路由解析
文件位置:packages/core/router/routes-resolver.ts
export class RoutesResolver implements Resolver {
public resolve<T extends HttpServer>(
applicationRef: T,
globalPrefix: string,
) {
const modules = this.container.getModules();
// 遍历所有模块
modules.forEach(({ controllers, metatype }, moduleName) => {
const modulePath = this.getModulePathMetadata(metatype)!;
this.registerRouters(
controllers, moduleName, globalPrefix, modulePath, applicationRef,
);
});
}
public registerRouters(routes, moduleName, globalPrefix, modulePath, applicationRef) {
routes.forEach(instanceWrapper => {
const { metatype } = instanceWrapper;
// 读取 @Controller 的 host 元数据
const host = this.getHostMetadata(metatype!);
// 提取 Controller 路径前缀
const routerPaths = this.routerExplorer.extractRouterPath(metatype as Type<any>);
// 读取 Controller 级版本
const controllerVersion = this.getVersionMetadata(metatype!);
routerPaths.forEach(path => {
// 组合路由路径元数据
const routePathMetadata: RoutePathMetadata = {
ctrlPath: path,
modulePath,
globalPrefix,
controllerVersion,
versioningOptions: this.applicationConfig.getVersioning(),
};
// 交给 RouterExplorer 探索并注册方法级路由
this.routerExplorer.explore(
instanceWrapper, moduleName, applicationRef, host!, routePathMetadata,
);
});
});
}
}
4.5 PathsExplorer — 扫描方法元数据
文件位置:packages/core/router/paths-explorer.ts
export class PathsExplorer {
public scanForPaths(instance: Controller): RouteDefinition[] {
const instancePrototype = Object.getPrototypeOf(instance);
return this.metadataScanner
.getAllMethodNames(instancePrototype)
.reduce((acc, method) => {
const route = this.exploreMethodMetadata(instance, instancePrototype, method);
if (route) {
acc.push(route);
}
return acc;
}, [] as RouteDefinition[]);
}
public exploreMethodMetadata(instance, prototype, methodName): RouteDefinition | null {
const prototypeCallback = prototype[methodName];
// 读取 @Get/@Post 等设置的元数据
const routePath = Reflect.getMetadata(PATH_METADATA, prototypeCallback);
if (isUndefined(routePath)) {
return null; // 没有路由元数据 → 不是路由处理器
}
const requestMethod = Reflect.getMetadata(METHOD_METADATA, prototypeCallback);
const version = Reflect.getMetadata(VERSION_METADATA, prototypeCallback);
return {
path: isString(routePath) ? [addLeadingSlash(routePath)] : routePath.map(addLeadingSlash),
requestMethod,
targetCallback: instance[methodName],
methodName,
version,
};
}
}
核心逻辑总结:
MetadataScanner遍历原型链上的所有方法名- 对每个方法,尝试读取
PATH_METADATA— 没有则跳过(说明不是路由处理器) - 读取
METHOD_METADATA和VERSION_METADATA - 返回
RouteDefinition对象,包含路径、HTTP 方法、回调函数、版本
4.6 元数据流全景图
编译时(装饰器执行) 运行时(路由注册)
───────────────── ─────────────────
@Controller('cats') RoutesResolver
↓ Reflect.defineMetadata │
├ CONTROLLER_WATERMARK = true ├→ Reflect.getMetadata(PATH_METADATA, CatsController)
├ PATH_METADATA = 'cats' │ → 'cats'
├ HOST_METADATA = undefined ├→ Reflect.getMetadata(HOST_METADATA, CatsController)
└ VERSION_METADATA = '1' │ → undefined
└→ Reflect.getMetadata(VERSION_METADATA, CatsController)
@Get(':id') → '1'
↓ Reflect.defineMetadata
├ PATH_METADATA = ':id' PathsExplorer
└ METHOD_METADATA = RequestMethod.GET │
├→ Reflect.getMetadata(PATH_METADATA, findOne)
│ → ':id'
└→ Reflect.getMetadata(METHOD_METADATA, findOne)
→ RequestMethod.GET
最终注册:Express.get('/v1/cats/:id', handler)
五、RESTful API 设计
[架构] 本节面向技术负责人和架构师。
5.1 资源命名规范
RESTful API 将一切抽象为资源,URL 应使用复数名词表示资源集合:
// 正确 — 复数名词
GET /users → 获取用户列表
GET /users/123 → 获取单个用户
POST /users → 创建用户
PUT /users/123 → 全量更新用户
PATCH /users/123 → 部分更新用户
DELETE /users/123 → 删除用户
// 错误示范
GET /getUsers → 不要用动词
GET /user → 不要用单数
POST /createUser → HTTP 方法已表达了动作
GET /user/get-profile → 避免嵌套动词
嵌套资源用于表示从属关系:
GET /users/123/orders → 用户 123 的订单列表
GET /users/123/orders/456 → 用户 123 的订单 456
POST /users/123/orders → 为用户 123 创建订单
5.2 HTTP 方法语义
| 方法 | 幂等性 | 安全性 | 请求体 | 典型用途 |
|---|---|---|---|---|
| GET | 是 | 是 | 无 | 查询资源 |
| POST | 否 | 否 | 有 | 创建资源、触发操作 |
| PUT | 是 | 否 | 有 | 全量替换(客户端提供完整数据) |
| PATCH | 否 | 否 | 有 | 部分更新(只提供变更字段) |
| DELETE | 是 | 否 | 可选 | 删除资源 |
| HEAD | 是 | 是 | 无 | 同 GET,但不返回 body |
| OPTIONS | 是 | 是 | 无 | 查询支持的 HTTP 方法 |
- 幂等性:多次执行与一次执行结果相同(GET、PUT、DELETE)
- 安全性:不修改服务端状态(GET、HEAD、OPTIONS)
5.3 状态码策略
// 成功响应
@Post()
@HttpCode(HttpStatus.CREATED) // 201 — 资源已创建
create() {}
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) // 204 — 删除成功,无返回体
remove() {}
@Post('batch')
@HttpCode(HttpStatus.ACCEPTED) // 202 — 异步任务已接受
batchProcess() {}
// 错误响应(通常由异常过滤器处理)
throw new BadRequestException('Invalid data'); // 400
throw new UnauthorizedException('Token expired'); // 401
throw new ForbiddenException('No permission'); // 403
throw new NotFoundException('Cat not found'); // 404
throw new ConflictException('Email already exists'); // 409
5.4 全局路由前缀
在 main.ts 中设置全局前缀,避免在每个 Controller 上重复写:
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// 所有路由自动加上 /api 前缀
app.setGlobalPrefix('api');
// 排除特定路由
app.setGlobalPrefix('api', {
exclude: [
'health', // GET /health(不加前缀)
{ path: 'metrics', method: RequestMethod.GET },
],
});
await app.listen(3000);
}
// 结果:GET /api/cats, GET /api/users, GET /health
配合 URI 版本控制:
app.setGlobalPrefix('api');
app.enableVersioning({ type: VersioningType.URI });
// 结果:GET /api/v1/cats, GET /api/v2/cats
5.5 CRUD 资源生成器
NestJS CLI 提供一键生成完整 CRUD 资源的命令:
nest g resource cats
选择 REST API 后,自动生成:
src/cats/
├── cats.controller.ts # Controller(含 CRUD 路由)
├── cats.controller.spec.ts # Controller 测试
├── cats.service.ts # Service(含 CRUD 方法)
├── cats.service.spec.ts # Service 测试
├── cats.module.ts # Module
├── dto/
│ ├── create-cat.dto.ts # 创建 DTO
│ └── update-cat.dto.ts # 更新 DTO(PartialType)
└── entities/
└── cat.entity.ts # 实体类
生成的 Controller 骨架:
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}
@Get()
findAll() {
return this.catsService.findAll();
}
@Get(':id')
findOne(@Param('id') id: string) {
return this.catsService.findOne(+id);
}
@Patch(':id')
update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
return this.catsService.update(+id, updateCatDto);
}
@Delete(':id')
remove(@Param('id') id: string) {
return this.catsService.remove(+id);
}
}
5.6 完整的 RESTful Controller 示例
import {
Controller, Get, Post, Put, Patch, Delete,
Param, Query, Body, HttpCode, HttpStatus, Header,
ParseIntPipe, DefaultValuePipe,
} from '@nestjs/common';
import { CatsService } from './cats.service';
import { CreateCatDto } from './dto/create-cat.dto';
import { UpdateCatDto } from './dto/update-cat.dto';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
// POST /cats → 201 Created
@Post()
create(@Body() createCatDto: CreateCatDto) {
return this.catsService.create(createCatDto);
}
// GET /cats?page=1&limit=10 → 200 OK
@Get()
findAll(
@Query('page', new DefaultValuePipe(1), ParseIntPipe) page: number,
@Query('limit', new DefaultValuePipe(10), ParseIntPipe) limit: number,
) {
return this.catsService.findAll({ page, limit });
}
// GET /cats/123 → 200 OK
@Get(':id')
findOne(@Param('id', ParseIntPipe) id: number) {
return this.catsService.findOne(id);
}
// PUT /cats/123 → 200 OK
@Put(':id')
replace(
@Param('id', ParseIntPipe) id: number,
@Body() createCatDto: CreateCatDto,
) {
return this.catsService.replace(id, createCatDto);
}
// PATCH /cats/123 → 200 OK
@Patch(':id')
update(
@Param('id', ParseIntPipe) id: number,
@Body() updateCatDto: UpdateCatDto,
) {
return this.catsService.update(id, updateCatDto);
}
// DELETE /cats/123 → 204 No Content
@Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id', ParseIntPipe) id: number) {
return this.catsService.remove(id);
}
// HEAD /cats → 200 OK (headers only)
@Head()
@Header('X-Total-Count', '100')
head() {
return;
}
}
六、课后实践
练习 1:创建 CRUD 控制器(基础)
使用 CLI 生成 dogs 资源并验证所有路由:
nest g resource dogs
# 启动后用 curl 测试
curl http://localhost:3000/dogs # GET 列表
curl -X POST http://localhost:3000/dogs \
-H "Content-Type: application/json" \
-d '{"name":"Buddy","breed":"Labrador"}' # POST 创建
curl http://localhost:3000/dogs/1 # GET 单个
curl -X PATCH http://localhost:3000/dogs/1 \
-H "Content-Type: application/json" \
-d '{"name":"Max"}' # PATCH 更新
curl -X DELETE http://localhost:3000/dogs/1 # DELETE 删除
练习 2:参数装饰器全家桶(基础)
在一个 Controller 中,至少使用以下 6 种参数装饰器:
@Param()— 路由参数@Query()— 查询参数@Body()— 请求体@Headers()— 请求头@Ip()— 客户端 IP@Req()— 原始请求对象
练习 3:版本控制实战(高阶)
创建一个同时支持 v1 和 v2 的 users 模块:
// v1 返回基本信息
@Controller({ path: 'users', version: '1' })
export class UsersControllerV1 { /* ... */ }
// v2 返回扩展信息(含 avatar、lastLogin)
@Controller({ path: 'users', version: '2' })
export class UsersControllerV2 { /* ... */ }
测试:
curl http://localhost:3000/v1/users/1
curl http://localhost:3000/v2/users/1
练习 4:阅读路由注册源码(资深)
打开 packages/core/router/paths-explorer.ts,阅读 scanForPaths() 和 exploreMethodMetadata() 方法,回答:
MetadataScanner.getAllMethodNames()如何获取类的所有方法?- 一个方法如果没有
@Get()/@Post()等装饰器,exploreMethodMetadata()会返回什么? PATH_METADATA的值是存在方法本身上还是原型上?为什么使用prototypeCallback而非instanceCallback来读取元数据?
练习 5:设计一个 RESTful API(架构)
为一个"博客系统"设计完整的 RESTful API,包括:
- 文章(Posts)的 CRUD
- 评论(Comments)的 CRUD(作为文章的子资源)
- 标签(Tags)的 CRUD
- 正确的 HTTP 方法和状态码
- 分页、过滤、排序参数设计
七、本课知识点总结
| 知识点 | 要点 |
|---|---|
| @Controller() | 标记类为控制器,可选路径前缀、host、version、scope |
| HTTP 方法装饰器 | @Get/@Post/@Put/@Delete/@Patch/@Options/@Head/@All |
| 参数装饰器 | @Param/@Query/@Body/@Headers/@Ip/@Req/@Res/@Session/@HostParam |
| DTO | 使用 class 定义请求体结构,配合 class-validator 做验证 |
| 响应控制 | @HttpCode() 状态码、@Header() 响应头、@Redirect() 重定向 |
| 路由通配符 | 基于 path-to-regexp,支持通配符和正则约束 |
| 子域名路由 | @Controller({ host: ':account.example.com' }) + @HostParam() |
| 异步 | async/await 和 RxJS Observable 两种方式 |
| @Res() 模式 | 直接操作 res 对象,passthrough 模式保持框架响应能力 |
| 版本控制 | 4 种策略(URI/Header/Media Type/Custom),Controller 级和路由级 |
| VERSION_NEUTRAL | 版本无关路由,不受版本控制策略影响 |
| 源码机制 | @Controller → 类元数据、@Get → 方法元数据 → PathsExplorer 扫描 → RoutesResolver 注册 |
| 全局前缀 | app.setGlobalPrefix('api'),可排除特定路由 |
| CRUD 生成 | nest g resource 一键生成完整 CRUD 骨架 |
下一课预告:第三课将深入 Provider 与依赖注入,学习 @Injectable()、自定义 Provider、注入作用域、循环依赖处理等 NestJS 的核心机制。