Опирается на правила: R-ALIAS-1..3, R-ACT-1..4 и X-коды из REST API Style Guide → раздел Alias и Action-эндпоинты.

Важно знать

  • me — только когда эндпоинт принимает и свой, и чужой ID (admin scope).
  • Граница me: «может ли супер-админ обратиться по другому ID?» Да → me нужен.
  • Alias-параметр ':id' в NestJS может принять строку 'me' — маршруты сортируются по специфичности.
  • Временные alias (latest, current) — отдельный @Get('latest') до @Get(':id').
  • Action@Post(':id/confirm') + @HttpCode(200); имя — глагол в инфинитиве.
  • @ApiOperation({ operationId }) action-метода относится к тегу родительского ресурса.
  • /api/v1/me без users/ префикса — запрещён.

CRUD не покрывает все доменные операции. NestJS реализует alias через порядок объявления маршрутов и action-эндпоинты через @Post с вложенным сегментом.

Alias-сегменты

me

R-ALIAS-1: в эндпоинтах, где admin может обратиться по чужому ID.

@Controller('users')
@ApiTags('Users')
export class UsersController {

  @Get(':id')
  @ApiOperation({ operationId: 'getUser', summary: 'Get user by id or alias' })
  findOne(@Param('id') id: string, @CurrentUser() actor: Actor) {
    const resolvedId = id === 'me' ? actor.id : id;
    return this.usersService.findOne(resolvedId);
  }
}

Маршрут @Get(':id') принимает и 550e8400-... и строку 'me'. Логика разрешения alias — в use case, не в контроллере.

Граничный тест: «может ли супер-админ обратиться по другому ID к этому же эндпоинту?»

  • ДаGET /users/:id и GET /users/me — один маршрут, alias в параметре.
  • Нет → singleton (/profile), me избыточен.

Временные и порядковые alias

R-ALIAS-2: отдельный маршрут до параметрического.

@Controller('deployments')
@ApiTags('Deployments')
export class DeploymentsController {

  @Get('latest')               // ✓ — объявлен ДО ':id', иначе NestJS матчит 'latest' как id
  @ApiOperation({ operationId: 'getLatestDeployment', summary: 'Get latest deployment' })
  findLatest() {
    return this.deploymentsService.findLatest();
  }

  @Get(':id')
  @ApiOperation({ operationId: 'getDeployment', summary: 'Get deployment by id' })
  findOne(@Param('id') id: string) {
    return this.deploymentsService.findOne(id);
  }
}

Порядок объявления — критичен в NestJS: строковый маршрут ('latest') должен идти перед параметрическим (':id').

Другие примеры:

@Get('current')    // GET /api/v1/subscriptions/current
@Get('next')       // GET /api/v1/invoices/next
@Get('previous')   // GET /api/v1/billing-periods/previous

Логические alias

R-ALIAS-3: по бизнес-признаку.

@Controller('payment-methods')
@ApiTags('PaymentMethods')
export class PaymentMethodsController {

  @Get('default')
  @ApiOperation({ operationId: 'getDefaultPaymentMethod', summary: 'Get default payment method' })
  findDefault(@CurrentUser() user: Actor) {
    return this.paymentMethodsService.findDefault(user.id);
  }

  @Get(':id')
  findOne(@Param('id') id: string) {}
}

Аналогично: 'primary', 'active', 'draft'.

Запреты для me

R-ALIAS-X1: me где не нужен.

// ✗ — orders уже из контекста токена
@Get('users/me/orders')

// ✓ — заказы текущего пользователя
@Controller('orders')
@Get()
findAll(@CurrentUser() user: Actor) {
  return this.ordersService.findByCustomer(user.id);
}

R-ALIAS-X2: me без users/:

// ✗ отдельный контроллер /api/v1/me
@Controller('me')
export class MeController {}

// ✓ — alias внутри UsersController
@Controller('users')
@Get(':id')
findOne(@Param('id') id: string) { /* 'me' через alias */ }

Action-эндпоинты

R-ACT-1..4:

@Controller('orders')
@ApiTags('Orders')
export class OrdersController {

  @Post(':id/confirm')
  @HttpCode(200)                // action возвращает 200, не 201
  @ApiOperation({ operationId: 'confirmOrder', summary: 'Confirm order' })
  confirm(@Param('id') id: string) {
    return this.ordersService.confirm(id);
  }

  @Post(':id/cancel')
  @HttpCode(200)
  @ApiOperation({ operationId: 'cancelOrder', summary: 'Cancel order' })
  cancel(
    @Param('id') id: string,
    @Body() dto: CancelOrderDto,
  ) {
    return this.ordersService.cancel(id, dto.reason);
  }

  @Post(':id/ship')
  @HttpCode(200)
  @ApiOperation({ operationId: 'shipOrder', summary: 'Ship order' })
  ship(
    @Param('id') id: string,
    @Body() dto: ShipOrderDto,
  ) {
    return this.ordersService.ship(id, dto);
  }
}

Параметры action-эндпоинта:

  • Имя — глагол в инфинитиве (confirm, cancel, ship, refund).
  • Метод — всегда @Post + @HttpCode(200).
  • Параметры — в @Body(). Если параметров нет — пустое тело допустимо.
  • operationId в camelCase относится к тегу родителя (Orders).

ShipOrderDto с параметрами действия:

export class ShipOrderDto {
  @IsString()
  trackingNumber: string;

  @IsString()
  carrier: string;
}

Когда action vs PATCH

// Меняем description — нет бизнес-правил → PATCH
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateOrderDto) {}

// Меняем status через state-machine → Action
@Post(':id/confirm')
@HttpCode(200)
confirm(@Param('id') id: string) {}

Action делает семантику явной в логах: POST /orders/123/confirm вместо PATCH /orders/123.

Что запрещено

АнтипаттернПравилоЧто взамен
me где endpoint singletonR-ALIAS-X1singleton без alias
@Controller('me') без users/R-ALIAS-X2alias в UsersController
@Get(':id') до @Get('latest')R-ALIAS-2строковый маршрут первым
@Post(':id/confirmation') существительноеR-ACT-X1'confirm'
@Post(':id/confirmed') причастиеR-ACT-X1'confirm'
@Put(':id/confirm')R-ACT-X2@Post
@Patch(':id') { status: 'CONFIRMED' } для командыR-ACT-1@Post(':id/confirm')
Action без @HttpCode(200)R-RSP-6@HttpCode(200) обязателен

Куда дальше

  • URL и ресурсы — формат пути, декораторы контроллеров.
  • JSON и формат ответов — 200 + обновлённый ресурс для action.
  • OpenAPI и антипаттерны — operationId для action.
  • Версионирование — action в v2.
  • REST API Style Guide (нормативно) — формулировки.