Skip to content
第 172 / 250 章Node⏱ 10 分钟阅读

第 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  ❌ 404

2.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/users

5.3 Header 版本

typescript
app.enableVersioning({
  type: VersioningType.HEADER,
  header: 'X-API-Version',
});
// 请求头:X-API-Version: 2

5.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()

动手练习

  1. 实现一个 /users/:id(\\d+) 只接受数字 ID 的路由
  2. 给项目添加 URI 版本控制
  3. SetMetadata 实现一个 @Public() 装饰器
  4. 实现一个 /files/* 路径,匹配任意子路径

推荐阅读


下一章:第 173 章:管道(Pipe)与数据转换

本站基于 VitePress 构建 · 由 Codebook 团队维护