Опирается на правила:
R-VER-1..6иR-VER-X1..X4из REST API Style Guide → раздел Версионирование.
Важно знать
VersioningType.URIвenableVersioning→/api/v1/...автоматически.setGlobalPrefix('api')+defaultVersion: '1'— минимальная конфигурация.- Новая версия только при breaking change. Non-breaking идут в текущую.
- Клиент обязан игнорировать неизвестные enum-значения и поля в ответе.
- Параллельная поддержка
v1/v2— отдельные контроллеры, общие use cases, разные DTO.- Версия в query (
?version=1) — запрещена (R-VER-X2).- Минорная версия (
v1.2) — запрещена (R-VER-X1).
REST API — публичный контракт. NestJS предоставляет встроенный механизм URI-версионирования, который изолирует версии без ручного роутинга.
Настройка
R-VER-1..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);
}
Результат: @Controller('orders') без явной версии → /api/v1/orders.
/api/v1/orders ✓
/api/v1/orders/:id ✓
/api/v2/orders ✓ (новая версия контроллера)
/api/v1.2/orders ✗ — минорная (R-VER-X1)
/api/2024/orders ✗ — дата (R-VER-X1)
/orders ✗ — без /api и без версии (R-VER-X3)
/v1/orders ✗ — без /api (R-VER-X3)
Версия на контроллере
Явная версия через декоратор @Version:
import { Controller, Get, Version } from '@nestjs/common';
@Controller('orders')
@Version('1')
@ApiTags('Orders')
export class OrdersControllerV1 {
@Get(':id')
@ApiOperation({ operationId: 'getOrderV1', summary: 'Get order (v1)' })
findOne(@Param('id') id: string) {
return this.ordersService.findOne(id);
}
}
@Controller('orders')
@Version('2')
@ApiTags('Orders')
export class OrdersControllerV2 {
@Get(':id')
@ApiOperation({ operationId: 'getOrderV2', summary: 'Get order (v2)' })
findOne(@Param('id') id: string) {
return this.ordersService.findOne(id); // тот же use case
}
}
Оба контроллера регистрируются в AppModule. Use case — общий; DTO и маппинг — разные.
R-VER-5 — клиент игнорирует неизвестное
Клиент API обязан:
- Игнорировать неизвестные значения enum (или обрабатывать как
UNKNOWN). - Игнорировать неизвестные поля в ответе JSON.
// ✓ TypeScript-клиент с forward-compat
interface OrderStatus {
status: 'CREATED' | 'CONFIRMED' | 'SHIPPED' | string; // string — неизвестные значения
}
// ✓ десериализация игнорирует новые поля по умолчанию
const order = await fetch('/api/v1/orders/123').then(r => r.json());
// новые поля просто присутствуют в объекте — не ломают код
На этом основано R-VER-6: добавление нового optional поля — non-breaking.
R-VER-6 — breaking vs non-breaking
Breaking — требуют v2
| Изменение | Пример |
|---|---|
| Удаление endpoint | удалили DELETE /api/v1/orders/:id |
| Удаление/переименование поля DTO | customerId → userId |
| Удаление/переименование query-параметра | ?customerId= → ?userId= |
| Изменение типа поля | string → number |
| Изменение формата поля | date → date-time |
| Удаление значения enum | убрали OrderStatus.DRAFT |
| Изменение HTTP-метода | POST /orders → PUT /orders |
| Обязательный новый параметр запроса | добавили required Idempotency-Key |
| Изменение URL-пути | /orders → /sales-orders |
| Ужесточение валидации | maxLength: 100 → maxLength: 50 |
Non-breaking — в текущую версию
| Изменение | Пример |
|---|---|
| Новое optional поле в ответе | добавили metadata в OrderResponse |
| Необязательный новый query-параметр | ?channel=web |
| Новое значение enum | OrderStatus.RESERVED |
| Новый endpoint | POST /api/v1/orders/:id/duplicate |
| Новый error code | ORDER_LIMIT_EXCEEDED в enum |
| Ослабление валидации | maxLength: 50 → maxLength: 100 |
Изменение текста detail / title | улучшили сообщение об ошибке |
R-VER-X4: не создавать v2 для добавления необязательного поля.
Параллельная поддержка v1 и v2
// orders.controller.v1.ts
@Controller('orders')
@Version('1')
export class OrdersControllerV1 {
constructor(private readonly ordersService: OrdersService) {}
@Get(':id')
async findOne(@Param('id') id: string): Promise<OrderResponseV1> {
const order = await this.ordersService.findOne(id);
return mapToV1(order);
}
}
// orders.controller.v2.ts
@Controller('orders')
@Version('2')
export class OrdersControllerV2 {
constructor(private readonly ordersService: OrdersService) {}
@Get(':id')
async findOne(@Param('id') id: string): Promise<OrderResponseV2> {
const order = await this.ordersService.findOne(id);
return mapToV2(order); // новый формат
}
}
OrdersService — общий. Маппинг mapToV1/mapToV2 — изолирует формат от логики.
После выхода v2:
- Пометить
v1deprecated (@ApiOperation({ deprecated: true })). - Добавить заголовки
SunsetиDeprecation(см. Rate limiting, файлы, deprecation). - Мониторить трафик на
v1. - После
Sunset→410 Gone.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Минорная версия /api/v1.2/ | R-VER-X1 | целое число (v2) |
Дата-версия /api/2024/ | R-VER-X1 | v1, v2 |
Версия в query ?version=1 | R-VER-X2 | в URL path |
Endpoint без /api | R-VER-X3 | setGlobalPrefix('api') |
| Endpoint без версии | R-VER-X3 | defaultVersion: '1' или @Version |
| Новая версия для optional поля | R-VER-X4 | в текущей версии |
Header versioning Accept-Version | R-VER-1 | VersioningType.URI |
VersioningType.HEADER или VersioningType.MEDIA_TYPE | R-VER-1 | только URI |
Куда дальше
- URL и ресурсы — формат
/api/v1/,setGlobalPrefix. - Rate limiting, файлы, deprecation —
Sunsetдля v1. - Ошибки RFC 9457 —
ErrorCodeenum — non-breaking расширение. - OpenAPI и антипаттерны —
operationIdпри параллельных версиях. - REST API Style Guide (нормативно) — формулировки.