← назад к разделу

CRUD-операций не всегда хватает. Клиенту нужен «текущий пользователь», «последнее развёртывание», «платёжный метод по умолчанию» — и это не фильтрация, а удобные сокращения. Отдельный класс запросов — доменные команды: подтвердить заказ, отменить, отправить. Для них есть два инструмента: alias-сегменты и action-эндпоинты.

Alias-сегменты: сокращения вместо идентификатора

me — текущий пользователь

Когда смотреть на профиль может и сам пользователь, и администратор с чужим ID, удобно использовать один маршрут GET /users/:id. Строка 'me' в этом случае работает как псевдоним: NestJS передаёт её в параметр id, контроллер проверяет значение и подставляет 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);
  }
}

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

  • Да → один маршрут GET /users/:id, me как alias в параметре.
  • Нет → отдельный singleton-ресурс /profile без alias.

Частая ошибка — заводить @Controller('me') как отдельный контроллер на верхнем уровне. Так делать не нужно: me — alias внутри UsersController, а не самостоятельный ресурс.

Ещё одна ошибка — использовать me там, где эндпоинт уже принадлежит текущему пользователю по контексту токена:

// Неверно: /users/me/orders избыточен, заказы и так привязаны к токену
@Get('users/me/orders')

// Верно: контроллер заказов, ID берётся из токена
@Controller('orders')
@Get()
findAll(@CurrentUser() user: Actor) {
  return this.ordersService.findByCustomer(user.id);
}

latest, current, next — временны́е и порядковые alias

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

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

  @Get('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);
  }
}

Аналогичная конструкция для других alias:

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

default, primary, active — логические alias

Когда среди ресурсов есть «главный» по бизнес-признаку, alias называет его явно:

@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'.

Action-эндпоинты: доменные команды

Некоторые операции нельзя свести к CRUD. Подтверждение заказа — это не просто смена поля status, это доменное событие с бизнес-правилами. Для таких операций используют action-эндпоинты: @Post с глаголом в сегменте пути.

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

  @Post(':id/confirm')
  @HttpCode(200)
  @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. Не существительное (confirmation), не причастие (confirmed).
  • Метод — всегда @Post. Не @Put, не @Patch.
  • Код ответа@HttpCode(200). Действие выполнено, но ресурс не создан, поэтому 201 не подходит.
  • Тело запроса — в @Body(). Если параметров нет, пустое тело допустимо.
  • operationId — camelCase, относится к тегу родительского ресурса.

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

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

  @IsString()
  carrier: string;
}

Когда action, а когда PATCH

Простой критерий: есть ли бизнес-правила или смена состояния через конечный автомат?

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

// Меняем статус через конечный автомат → Action
@Post(':id/confirm')
@HttpCode(200)
confirm(@Param('id') id: string) {}

Action делает семантику видимой в логах: POST /orders/123/confirm понятнее, чем PATCH /orders/123 с { status: 'CONFIRMED' } в теле.

Коротко

  • me — alias в параметре ':id', только когда эндпоинт доступен как «по своему ID», так и «по чужому» (например, для администратора).
  • Отдельный контроллер @Controller('me') — неверный подход; alias живёт внутри UsersController.
  • Строковые маршруты ('latest', 'default') объявляют до параметрических (':id') — иначе NestJS их не найдёт.
  • Action-эндпоинт — @Post(':id/глагол') + @HttpCode(200). Метод @Patch остаётся для обновления полей без бизнес-правил.
  • Имя action-сегмента — глагол в инфинитиве; существительные и причастия — частая ошибка.

Что почитать дальше

  • URL и ресурсы — формат пути, декораторы контроллеров.
  • JSON и формат ответов — структура ответа для action.
  • OpenAPI и антипаттерны — operationId для action.
  • Версионирование — action в v2.