第 172 章:动态路由与子域
学习目标
- 掌握路由参数的高级用法
- 学会通配符与正则匹配
- 理解子域路由
- 学会路由前缀与版本控制
一、路由参数进阶
1.1 多段路径参数
typescript
@Controller('api')
export class ApiController {
// /api/orgs/:orgId/users/:userId
@Get('orgs/:orgId/users/:userId')
getUser(@Param('orgId') orgId: string, @Param('userId') userId: string) {
return { orgId, userId };
}
}1.2 可选参数
typescript
// ❌ NestJS 不支持 ? 形式
// ✅ 通过多个路由处理
@Get('users')
list(@Query('status') status?: string) {
return this.userService.findAll(status);
}
@Get('users/:id')
detail(@Param('id') id: string) {
return this.userService.findOne(id);
}1.3 全匹配参数
typescript
@Get('files/*path') // * 匹配任意路径段
getFile(@Param('path') path: string) {
// /files/a/b/c.txt → path = 'a/b/c.txt'
return { path };
}二、通配符与正则
2.1 数字 ID 约束
typescript
@Get(':id(\\d+)') // 只匹配数字
findById(@Param('id') id: string) {
return this.userService.findOne(Number(id));
}
// /api/users/123 ✅ 匹配
// /api/users/abc ❌ 4042.2 邮箱正则
typescript
@Get(':email([^\\s@]+@[^\\s@]+\\.[^\\s@]+)')
findByEmail(@Param('email') email: string) {
return this.userService.findByEmail(email);
}2.3 UUID 校验
typescript
import { ParseUUIDPipe } from '@nestjs/common';
@Get(':id')
findOne(@Param('id', new ParseUUIDPipe()) id: string) {
// 必须是合法 UUID,否则 400
return this.userService.findOne(id);
}2.4 多个路由共用 Handler
typescript
@Get(['list', 'list/:type'])
list(@Param('type') type?: string) {
// GET /list → type = undefined
// GET /list/abc → type = 'abc'
}三、路由顺序
路由定义顺序很重要!具体的放前面,通配的放后面。
typescript
@Controller('users')
export class UserController {
// ✅ 具体路由在前
@Get('me')
getCurrent() {
return this.userService.getCurrent();
}
@Get('search')
search(@Query('q') q: string) {
return this.userService.search(q);
}
// ✅ 通配在后
@Get(':id')
findOne(@Param('id') id: string) {
return this.userService.findOne(id);
}
}四、路由前缀
4.1 全局前缀
typescript
// main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api/v1'); // 所有路由加 /api/v1
await app.listen(3000);
}4.2 排除特定路由
typescript
app.setGlobalPrefix('api/v1', {
exclude: [{ path: 'health', method: RequestMethod.GET }],
});
// /api/v1/users
// /health ← 排除,不加前缀4.3 模块级前缀
typescript
@Controller({ path: 'users', version: '1' }) // 配合 VERSION_NEUTRAL
export class UserV1Controller {}
@Controller({ path: 'users', version: '2' })
export class UserV2Controller {}五、版本控制
5.1 启用版本控制
typescript
// main.ts
import { VersioningType } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.enableVersioning({
type: VersioningType.URI, // /v1/users /v2/users
defaultVersion: '1',
});
await app.listen(3000);
}5.2 URI 版本
typescript
@Controller({
path: 'users',
version: '1',
})
export class UsersV1Controller {
@Get()
findAll() {
return { version: 1, users: [] };
}
}
@Controller({
path: 'users',
version: '2',
})
export class UsersV2Controller {
@Get()
findAll() {
return { version: 2, users: [], pagination: {} };
}
}
// GET /v1/users
// GET /v2/users5.3 Header 版本
typescript
app.enableVersioning({
type: VersioningType.HEADER,
header: 'X-API-Version',
});
// 请求头:X-API-Version: 25.4 Media Type 版本
typescript
app.enableVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'v=', // Accept: application/json;v=2
});5.5 中性版本
typescript
import { VERSION_NEUTRAL } from '@nestjs/common';
@Controller({
path: 'health',
version: VERSION_NEUTRAL,
})
export class HealthController {
@Get()
health() {
return { status: 'ok' };
}
}
// 任意版本都能访问 /health六、子域路由
6.1 基于 Host 匹配
typescript
import { Controller, Get, Host } from '@nestjs/common';
@Controller({ host: 'admin.example.com' })
export class AdminController {
@Get()
index() {
return { msg: 'Admin Dashboard' };
}
}
@Controller({ host: ':tenant.example.com' })
export class TenantController {
@Get()
index(@HostParam('tenant') tenant: string) {
return { tenant };
}
}6.2 动态子域
typescript
@Controller({ host: ':subdomain.example.com' })
export class AppController {
@Get()
root(@HostParam('subdomain') subdomain: string) {
return { subdomain };
}
}
// admin.example.com → subdomain = 'admin'
// api.example.com → subdomain = 'api'七、路由元数据
7.1 自定义元数据
typescript
import { SetMetadata } from '@nestjs/common';
export const Public = () => SetMetadata('isPublic', true);
@Controller('auth')
export class AuthController {
@Public() // 标记为公开,跳过 JWT 校验
@Post('login')
login(@Body() dto: LoginDto) {
return this.authService.login(dto);
}
}7.2 在 Guard 中读取
typescript
@Injectable()
export class JwtAuthGuard implements CanActivate {
canActivate(ctx: ExecutionContext): boolean {
const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [
ctx.getHandler(),
ctx.getClass(),
]);
if (isPublic) return true;
// 校验 JWT...
}
}八、子资源路由
typescript
@Controller('users')
export class UserController {
// GET /users/:id/posts
@Get(':id/posts')
getUserPosts(@Param('id') userId: string) {
return this.postService.findByUser(userId);
}
// POST /users/:id/posts
@Post(':id/posts')
createPost(@Param('id') userId: string, @Body() dto: CreatePostDto) {
return this.postService.create(userId, dto);
}
// GET /users/:id/posts/:postId
@Get(':id/posts/:postId')
getPost(@Param('id') userId: string, @Param('postId') postId: string) {
return this.postService.findOne(userId, postId);
}
}九、Express 风格的通配
typescript
// express 风格的 * 通配
@Get('static/*')
serveStatic(@Req() req: Request, @Res() res: Response) {
return res.sendFile(path.join(__dirname, req.path));
}十、本章小结
| 用法 | 写法 |
|---|---|
| 路径参数 | :id |
| 可选参数 | 多个路由 |
| 通配参数 | * |
| 正则约束 | :id(\\d+) |
| 全局前缀 | setGlobalPrefix() |
| URI 版本 | enableVersioning |
| 子域 | { host: ':tenant.example.com' } |
| 元数据 | SetMetadata() |
动手练习
- 实现一个
/users/:id(\\d+)只接受数字 ID 的路由 - 给项目添加 URI 版本控制
- 用
SetMetadata实现一个@Public()装饰器 - 实现一个
/files/*路径,匹配任意子路径
推荐阅读
下一章:第 173 章:管道(Pipe)与数据转换 →