Опирается на правила: R-FLD-1..7, R-RSP-1..8 и X-коды из REST API Style Guide → раздел JSON и формат ответов.

Важно знать

  • camelCase нативен для TypeScript — поля TS-класса = поля JSON без настройки.
  • Date сериализуется в ISO 8601 при JSON.stringify автоматически.
  • Enumstring enum с UPPER_SNAKE_CASE значениями ('CONFIRMED', не 0).
  • null в 2xx запрещён — незаполненное поле должно быть undefined (выпадает из JSON).
  • null в PATCH body — команда удалить поле (JSON Merge Patch RFC 7396).
  • Envelope ({ success: true, data: ... }) — запрещён.
  • Коллекция{ "content": [...] } + метаданные пагинации (не envelope).
  • Пустые коллекции[], не null.

NestJS с TypeScript делает большинство правил следствием языка: camelCase полей, ISO 8601 для Date, UPPER_SNAKE_CASE через string enum. Нужно явно позаботиться только об исключении null из ответов.

Имена полей

R-FLD-1..5:

export class OrderResponse {
  orderId: string;              // ✓ camelCase + Id-суффикс
  customerId: string;           // ✓
  createdAt: Date;              // ✓ → "2026-05-26T10:30:00Z" в JSON
  totalAmount: number;          // ✓ camelCase
  status: OrderStatus;          // ✓ enum UPPER_SNAKE_CASE
  items: OrderItemResponse[];   // ✓ коллекция — множественное число
}
// ✗ антипаттерны именования
customer_id: string;           // snake_case — R-FLD-1
created_at: Date;              // snake_case — R-FLD-2
status: string;                // raw string вместо enum — R-FLD-3
id: string;                    // без суффикса Id — R-FLD-5

Enum — UPPER_SNAKE_CASE

R-FLD-3:

export enum OrderStatus {
  CREATED = 'CREATED',
  CONFIRMED = 'CONFIRMED',
  SHIPPED = 'SHIPPED',
  DELIVERED = 'DELIVERED',
  CANCELLED = 'CANCELLED',
}

export enum PaymentMethod {
  CREDIT_CARD = 'CREDIT_CARD',
  BANK_TRANSFER = 'BANK_TRANSFER',
  SBP = 'SBP',
}

String enum гарантирует корректный JSON без дополнительных трансформеров.

null → undefined

R-RSP-X1: null в 2xx запрещён. Незаполненное поле — undefined, не null.

export class OrderResponse {
  orderId: string;
  status: OrderStatus;
  comment?: string;            // ✓ optional — undefined, если не заполнено
  // comment: string | null    // ✗ null попадёт в JSON
}

undefined выпадает из JSON при сериализации. null — остаётся. Разница критична:

const order = { orderId: '123', status: 'CONFIRMED', comment: undefined };
JSON.stringify(order);
// → {"orderId":"123","status":"CONFIRMED"}   ✓ comment отсутствует

const order2 = { orderId: '123', status: 'CONFIRMED', comment: null };
JSON.stringify(order2);
// → {"orderId":"123","status":"CONFIRMED","comment":null}   ✗ нарушение R-RSP-X1

При маппинге из доменного объекта:

function mapToOrderResponse(order: Order): OrderResponse {
  return {
    orderId: order.id,
    status: order.status,
    comment: order.comment ?? undefined,  // null → undefined
  };
}

null в PATCH body — удалить поле

R-FLD-6: семантика запроса, не ответа.

PATCH /api/v1/orders/:id
Content-Type: application/merge-patch+json

{ "comment": null }

null в теле PATCH — команда удалить поле comment. Это JSON Merge Patch (RFC 7396).

export class UpdateOrderDto {
  @IsOptional()
  @IsString()
  comment?: string | null;     // null допустим в request-DTO
}

Разрешить null только в request-DTO (PATCH body). В response-DTO — никогда.

Boolean

R-FLD-7: префикс is/has/can опционально, но единообразно.

export class ProductResponse {
  productId: string;
  active: boolean;             // ✓ без префикса
  hasDiscount: boolean;        // ✓ с префиксом has
  canRefund: boolean;          // ✓ с префиксом can
}

Главное — единый стиль в проекте.

Форматы ответов

R-RSP-1..6:

Единичный ресурс

@Get(':id')
async findOne(@Param('id') id: string): Promise<OrderResponse> {
  return this.ordersService.findOne(id);
}

Без envelope. OrderResponse — сам ресурс.

Коллекция — content + пагинация

export class PagedOrdersResponse {
  content: OrderResponse[];
  page: number;
  size: number;
  totalElements: number;
  totalPages: number;
}

@Get()
async findAll(@Query() query: PaginationDto): Promise<PagedOrdersResponse> {
  return this.ordersService.findAll(query);
}
{
  "content": [{ "orderId": "...", "status": "CONFIRMED" }],
  "page": 1,
  "size": 20,
  "totalElements": 243,
  "totalPages": 13
}

Создание — 201 + Location

R-RSP-3:

@Post()
@ApiOperation({ operationId: 'createOrder', summary: 'Create order' })
async create(
  @Body() dto: CreateOrderDto,
  @Res({ passthrough: true }) res: Response,
): Promise<OrderResponse> {
  const order = await this.ordersService.create(dto);
  res.location(`/api/v1/orders/${order.orderId}`);
  return order;
}

NestJS с @Post по умолчанию отдаёт 201. res.location(...) добавляет заголовок Location.

Удаление — 204

R-RSP-5:

@Delete(':id')
@HttpCode(204)                 // явный @HttpCode обязателен
async remove(@Param('id') id: string): Promise<void> {
  await this.ordersService.remove(id);
}

Action — 200

R-RSP-6:

@Post(':id/confirm')
@HttpCode(200)                 // явный @HttpCode обязателен (дефолт @Post — 201)
async confirm(@Param('id') id: string): Promise<OrderResponse> {
  return this.ordersService.confirm(id);
}

Пустые коллекции

R-RSP-7:

export class ProductResponse {
  productId: string;
  tags: string[] = [];         // ✓ пустой массив, не null
}

// При маппинге
tags: product.tags ?? [],      // null → []

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

АнтипаттернПравилоЧто взамен
comment: string \| null = null в responseR-RSP-X1comment?: string (undefined)
"" пустая строка вместо отсутствияR-RSP-X2поле отсутствует (undefined)
nullable: true в @ApiPropertyR-RSP-X3optional field
{ success: true, data: order } envelopeR-RSP-X4плоский OrderResponse
customer_id snake_caseR-FLD-1customerId
status: 'in_progress' строчныеR-FLD-3'IN_PROGRESS'
id: string без суффиксаR-FLD-5orderId, productId
tags: nullR-RSP-7tags: []
2026-05-26 10:30:00 без T и ZR-FLD-22026-05-26T10:30:00Z

Куда дальше

  • Query-параметры — структура content для коллекций.
  • Ошибки RFC 9457 — формат error response vs success.
  • Заголовки и трассировка — Location для 201.
  • Версионирование — добавление optional поля non-breaking.
  • REST API Style Guide (нормативно) — формулировки.