Опирается на правила: R-PRIN-1..4, R-URL-1..3, R-RES-1..3, R-MTH-1..6, R-NEST-1..4 из REST API Style Guide → раздел URL и ресурсы.

Важно знать

  • 4 принципа: предсказуемость, единообразие, читаемость, стабильность.
  • URL — публичный контракт: изменение = breaking change, требует v2.
  • HATEOAS-ссылки в теле запрещены — навигация описывается в OpenAPI.
  • setGlobalPrefix('api') + VersioningType.URI/api/v1/... из коробки.
  • NestJS не редиректит trailing slash — не объявлять пути со слешем.
  • Коллекции — множественное число (/orders), singleton — единственное (/profile).
  • HTTP-методы через декораторы; @HttpCode(204) на @Delete, @HttpCode(200) на action @Post.
  • Максимум 2 уровня вложенности. Глубже — flat resource с filter.

REST URL — первая документация API. Разработчик угадывает GET /orders/:id/items по аналогии с GET /orders. NestJS реализует правила через декораторы контроллеров и глобальную конфигурацию.

Глобальная конфигурация

R-VER-1..3 и R-URL-3:

// main.ts
import { VersioningType } from '@nestjs/common';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  app.setGlobalPrefix('api');
  app.enableVersioning({
    type: VersioningType.URI,
    defaultVersion: '1',
  });

  await app.listen(3000);
}

Результат: все контроллеры автоматически получают префикс /api/v1/.... Служебные эндпоинты — вне этой цепочки:

@Controller('health')
export class HealthController {
  @Get()
  check() { return { status: 'ok' }; }
}
// → GET /health (без /api, без версии)

Формат пути

R-URL-1..3:

@Controller('order-items')   // ✓ kebab-case, строчные
export class OrderItemsController {}

@Controller('OrderItems')    // ✗ заглавные
@Controller('order_items')   // ✗ snake_case
@Controller('orderItems')    // ✗ camelCase

NestJS не добавляет trailing slash сам — не объявлять пути вида 'orders/'.

@Get(':id')         // ✓ /api/v1/orders/:id
@Get(':id/')        // ✗ trailing slash
@Get('list.json')   // ✗ расширение
@Get('getAll')      // ✗ глагол

Ресурсы

R-RES-1..3:

@Controller('orders')          // ✓ коллекция — множественное
export class OrdersController {}

@Controller('products')        // ✓
export class ProductsController {}

@Controller('order')           // ✗ единственное для коллекции
export class OrderController {}

Singleton-ресурс (один на пользователя):

@Controller('profile')         // ✓ singleton — единственное
export class ProfileController {
  @Get()                       // GET /api/v1/profile
  get() {}
}

Имя ресурса — доменный термин из Ubiquitous Language:

  • Order/orders, не /purchases, /transactions.
  • DeliveryAddress/delivery-addresses, не /shipping-info.
  • Customer/customers, не /users, /clients.

HTTP-методы

R-MTH-1..6 через декораторы:

@Controller('orders')
export class OrdersController {

  @Get()                       // GET /api/v1/orders → 200
  findAll() {}

  @Get(':id')                  // GET /api/v1/orders/:id → 200
  findOne(@Param('id') id: string) {}

  @Post()                      // POST /api/v1/orders → 201 (дефолт NestJS)
  create(@Body() dto: CreateOrderDto) {}

  @Put(':id')                  // PUT /api/v1/orders/:id → 200
  replace(@Param('id') id: string, @Body() dto: ReplaceOrderDto) {}

  @Patch(':id')                // PATCH /api/v1/orders/:id → 200
  update(@Param('id') id: string, @Body() dto: UpdateOrderDto) {}

  @Delete(':id')
  @HttpCode(204)               // DELETE → 204 No Content (дефолт NestJS — 200, нужен @HttpCode)
  remove(@Param('id') id: string) {}
}
ДекораторМетодДефолт NestJS@HttpCode нужен?
@GetGET200нет
@Post созданиеPOST201нет
@Put / @PatchPUT/PATCH200нет
@DeleteDELETE200да, 204
@Post actionPOST201да, 200

R-MTH-X1 — GET без побочных эффектов:

@Get(':id/cancel')   // ✗ GET с side-effect
cancel() {}

@Post(':id/cancel')  // ✓
@HttpCode(200)
cancel() {}

Вложенность

R-NEST-1..4:

@Controller('orders')
export class OrdersController {

  @Get(':orderId/items')              // ✓ 2 уровня
  getItems(@Param('orderId') orderId: string) {}

  @Get(':orderId/items/:itemId')      // ✓ 2 уровня + id
  getItem(
    @Param('orderId') orderId: string,
    @Param('itemId') itemId: string,
  ) {}
}

Path-параметры в коде именуются уникально (orderId, itemId) — требование Swagger/Redoc. В дизайн-документации используется {id}. Подробнее — OpenAPI и антипаттерны, правило R-OAS-3.

Глубже двух уровней — flat resource с filter:

// ✗ 3 уровня
@Get(':userId/orders/:orderId/items/:itemId')

// ✓ flat с filter
@Get()                                    // GET /api/v1/items?orderId=...
findAll(@Query('orderId') orderId: string) {}

ID в теле вместо пути — запрещён:

@Put(':id')                              // ✓
replace(@Param('id') id: string) {}

@Put()                                   // ✗ — id в body вместо path
replace(@Body() dto: { id: string }) {}

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

АнтипаттернПравилоЧто взамен
HATEOAS _links в теле ответаR-PRIN-X1OpenAPI описывает навигацию
@Controller('OrderItems')R-URL-X1'order-items'
@Get('orders/') trailing slashR-URL-X2без слеша
@Get('orders.json')R-URL-X3Accept: application/json
@Get('getOrders') глаголR-URL-X4@Get()
@Controller('order') для коллекцииR-RES-X1'orders'
@Get(':id') — GET с side-effectR-MTH-X1@Post(':id/action')
Вложенность более 2 уровнейR-NEST-X1flat с filter
ID в body вместо @ParamR-NEST-X2path-параметр

Куда дальше

  • Версионирование — enableVersioning, /api/v1/api/v2.
  • Alias и Action-эндпоинты — me, latest, доменные команды в NestJS.
  • Query-параметры — DTO-классы, @Query, пагинация.
  • OpenAPI и антипаттерны — operationId, именование path-параметров.
  • REST API Style Guide (нормативно) — формулировки правил.