← Back to the section

CRUD operations aren't always enough. A client needs the "current user", the "latest deployment", the "default payment method" — and this isn't filtering, it's convenient shortcuts. A separate class of requests is domain commands: confirm an order, cancel it, ship it. There are two tools for these: alias segments and action endpoints.

Alias segments: shortcuts instead of an identifier

me — the current user

When a profile can be viewed both by the user themselves and by an administrator with someone else's ID, it's convenient to use a single route GET /users/:id. The string 'me' in this case works as an alias: NestJS passes it into the id parameter, the controller checks the value and substitutes the ID from the token.

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

The question that helps you understand whether you need me: "can an administrator hit this same endpoint with someone else's ID?"

  • Yes → a single route GET /users/:id, with me as an alias in the parameter.
  • No → a separate singleton resource /profile without an alias.

A common mistake is to set up @Controller('me') as a separate top-level controller. Don't do that: me is an alias inside UsersController, not a standalone resource.

Another mistake is using me where the endpoint already belongs to the current user by the token context:

// Wrong: /users/me/orders is redundant, orders are already bound to the token
@Get('users/me/orders')

// Right: an orders controller, the ID comes from the token
@Controller('orders')
@Get()
findAll(@CurrentUser() user: Actor) {
  return this.ordersService.findByCustomer(user.id);
}

latest, current, next — temporal and ordinal aliases

Here the order in which routes are declared matters. In NestJS a string route must come before a parametric one — otherwise 'latest' will be handled as the value of :id.

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

  @Get('latest')               // declared BEFORE ':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);
  }
}

A similar construction for other aliases:

@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 — logical aliases

When there's a "main" one among the resources by some business attribute, the alias names it explicitly:

@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) {}
}

Likewise: 'primary', 'active', 'draft'.

Action endpoints: domain commands

Some operations can't be reduced to CRUD. Confirming an order isn't just changing the status field — it's a domain event with business rules. For such operations you use action endpoints: @Post with a verb in the path segment.

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

A few rules for action endpoints:

  • Segment name — a verb in the infinitive: confirm, cancel, ship, refund. Not a noun (confirmation), not a participle (confirmed).
  • Method — always @Post. Not @Put, not @Patch.
  • Response code@HttpCode(200). The action was performed, but no resource was created, so 201 doesn't fit.
  • Request body — in @Body(). If there are no parameters, an empty body is acceptable.
  • operationId — camelCase, belongs to the tag of the parent resource.

An example DTO with action parameters:

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

  @IsString()
  carrier: string;
}

When action, and when PATCH

A simple criterion: are there business rules or a state change through a state machine?

// Changing description — no business rules → PATCH
@Patch(':id')
update(@Param('id') id: string, @Body() dto: UpdateOrderDto) {}

// Changing status through a state machine → Action
@Post(':id/confirm')
@HttpCode(200)
confirm(@Param('id') id: string) {}

An action makes the semantics visible in the logs: POST /orders/123/confirm is clearer than PATCH /orders/123 with { status: 'CONFIRMED' } in the body.

In short

  • me — an alias in the ':id' parameter, only when the endpoint is accessible both "by your own ID" and "by someone else's" (for example, for an administrator).
  • A separate @Controller('me') is the wrong approach; the alias lives inside UsersController.
  • String routes ('latest', 'default') are declared before the parametric ones (':id') — otherwise NestJS won't find them.
  • An action endpoint is @Post(':id/verb') + @HttpCode(200). The @Patch method stays for updating fields without business rules.
  • The action segment name is a verb in the infinitive; nouns and participles are a common mistake.

Further reading

  • URL and resources — path format, controller decorators.
  • JSON and response format — response structure for an action.
  • OpenAPI and antipatterns — operationId for an action.
  • Versioning — actions in v2.