Пошук уроків, статей та іншого контенту
Порівняєте способи версіювання та налаштуєте сумісні версії маршрутів у NestJS.
API змінюється разом із вимогами до застосунку. Наприклад, у першій версії API поле name може бути рядком:
{
"name": "Кава"
}У новій версії формат може змінитися:
{
"title": "Кава",
"price": 80
}Якщо змінити маршрут без версіювання, старі клієнти можуть перестати працювати. Версіювання дає змогу:
зберігати старий контракт API;
поступово переносити клієнтів на нову версію;
випускати несумісні зміни без раптового порушення роботи застосунків;
чітко визначати, яку реалізацію маршруту викликати.
NestJS підтримує кілька способів версіювання маршрутів:
через URI;
через HTTP-заголовок;
через заголовок Accept із media type;
через власну стратегію версіювання.
Найпоширеніший спосіб — додати версію до шляху:
/api/v1/orders
/api/v2/ordersУ NestJS цей спосіб вмикається через enableVersioning() у файлі запуску застосунку.
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableVersioning({
type: VersioningType.URI,
});
await app.listen(3000);
}
bootstrap();Після цього NestJS очікує версію в URI. Якщо контролер має шлях orders, маршрут версії 1 буде доступний за адресою:
GET /api/v1/ordersЗа замовчуванням NestJS використовує префікс v. Його можна змінити:
app.enableVersioning({
type: VersioningType.URI,
prefix: 'version-',
});Тоді маршрут матиме вигляд:
/api/version-1/ordersЗазвичай використовують стандартний префікс v, оскільки він короткий і зрозумілий.
Версію можна призначити всьому контролеру:
import { Controller, Get } from '@nestjs/common';
@Controller({
path: 'orders',
version: '1',
})
export class OrdersV1Controller {
@Get()
findAll() {
return {
version: 1,
orders: ['Кава', 'Чай'],
};
}
}Якщо URI-версіювання увімкнено, маршрут буде таким:
GET /api/v1/ordersКонтролер для версії 2 може мати іншу реалізацію:
import { Controller, Get } from '@nestjs/common';
@Controller({
path: 'orders',
version: '2',
})
export class OrdersV2Controller {
@Get()
findAll() {
return {
version: 2,
orders: [
{ id: 1, title: 'Кава' },
{ id: 2, title: 'Чай' },
],
};
}
}Тепер NestJS розрізняє маршрути за версією:
GET /api/v1/orders
GET /api/v2/ordersОбидва маршрути мають однаковий базовий шлях, але повертають різні формати даних.
Не обов’язково створювати окремий контролер для кожної версії. Версію можна призначити конкретному методу за допомогою декоратора @Version():
import { Controller, Get, Version } from '@nestjs/common';
@Controller('orders')
export class OrdersController {
@Get()
@Version('1')
findAllV1() {
return {
version: 1,
orders: ['Кава', 'Чай'],
};
}
@Get()
@Version('2')
findAllV2() {
return {
version: 2,
orders: [
{ id: 1, title: 'Кава' },
{ id: 2, title: 'Чай' },
],
};
}
}Обидва методи мають однакові HTTP-метод і шлях, але NestJS розділяє їх за версією:
GET /api/v1/orders
GET /api/v2/ordersТакий підхід зручний, коли в одному контролері потрібно підтримувати кілька версій лише для окремих маршрутів.
Якщо реалізація маршруту сумісна з кількома версіями, можна передати масив версій:
import { Controller, Get, Version } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
@Version(['1', '2'])
check() {
return {
status: 'ok',
};
}
}Цей метод буде доступний за адресами:
GET /api/v1/health
GET /api/v2/healthЦе корисно для маршрутів, формат яких не змінюється між версіями. Наприклад, перевірка стану застосунку може мати однакову реалізацію в API версій 1 і 2.
Нижче наведено мінімальний приклад застосунку з двома версіями маршруту замовлень і одним сумісним маршрутом перевірки стану.
main.tsimport { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableVersioning({
type: VersioningType.URI,
});
await app.listen(3000);
}
bootstrap();orders.controller.tsimport { Controller, Get, Version } from '@nestjs/common';
@Controller('orders')
export class OrdersController {
@Get()
@Version('1')
findAllV1() {
return {
version: 1,
orders: ['Кава', 'Чай'],
};
}
@Get()
@Version('2')
findAllV2() {
return {
version: 2,
orders: [
{
id: 1,
title: 'Кава',
price: 80,
},
{
id: 2,
title: 'Чай',
price: 50,
},
],
};
}
}health.controller.tsimport { Controller, Get, Version } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
@Version(['1', '2'])
check() {
return {
status: 'ok',
};
}
}app.module.tsimport { Module } from '@nestjs/common';
import { OrdersController } from './orders.controller';
import { HealthController } from './health.controller';
@Module({
controllers: [OrdersController, HealthController],
})
export class AppModule {}Після запуску застосунку маршрути будуть такими:
GET /api/v1/orders
GET /api/v2/orders
GET /api/v1/health
GET /api/v2/healthПриклад запитів:
curl http://localhost:3000/api/v1/orders
curl http://localhost:3000/api/v2/orders
curl http://localhost:3000/api/v2/healthЗапит без версії:
curl http://localhost:3000/api/ordersне знайде версіонований маршрут і зазвичай завершиться відповіддю 404 Not Found.
Замість URI версію можна передавати в окремому HTTP-заголовку. Для цього використовується VersioningType.HEADER.
import { NestFactory } from '@nestjs/core';
import { VersioningType } from '@nestjs/common';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableVersioning({
type: VersioningType.HEADER,
header: 'X-API-Version',
});
await app.listen(3000);
}
bootstrap();Тепер URI не містить версію:
GET /api/ordersВерсію потрібно передати через заголовок:
curl -H "X-API-Version: 1" http://localhost:3000/api/orders
curl -H "X-API-Version: 2" http://localhost:3000/api/ordersКонтролер при цьому може залишатися таким самим:
import { Controller, Get, Version } from '@nestjs/common';
@Controller('orders')
export class OrdersController {
@Get()
@Version('1')
findAllV1() {
return {
version: 1,
orders: ['Кава', 'Чай'],
};
}
@Get()
@Version('2')
findAllV2() {
return {
version: 2,
orders: [
{ id: 1, title: 'Кава' },
{ id: 2, title: 'Чай' },
],
};
}
}Перевага цього підходу — URI залишається незмінним. Недолік — версію складніше побачити та протестувати, оскільки вона міститься в заголовку, а не в адресі.
Версіювання через заголовок може бути доречним, якщо:
URI має залишатися стабільним;
клієнти вже використовують заголовки для вибору формату API;
версію потрібно приховати від структури URL.
Ще один варіант — передавати версію в заголовку Accept. У NestJS для цього використовується VersioningType.MEDIA_TYPE.
app.enableVersioning({
type: VersioningType.MEDIA_TYPE,
key: 'Accept',
});У такому режимі NestJS шукає версію в значенні заголовка після ключа v:
curl -H "Accept: application/json;v=1" \
http://localhost:3000/api/orders
curl -H "Accept: application/json;v=2" \
http://localhost:3000/api/ordersМаршрут у цьому випадку залишається однаковим:
GET /api/ordersВерсія визначається значенням заголовка Accept.
Цей підхід відповідає ідеї узгодження формату відповіді через HTTP-заголовки, але для повсякденного використання він менш очевидний, ніж URI-версіювання.
Деякі маршрути не повинні належати конкретній версії. Наприклад, маршрут перевірки доступності застосунку може бути спільним для всіх клієнтів.
Для цього використовується VERSION_NEUTRAL:
import {
Controller,
Get,
VERSION_NEUTRAL,
} from '@nestjs/common';
@Controller({
path: 'health',
version: VERSION_NEUTRAL,
})
export class HealthController {
@Get()
check() {
return {
status: 'ok',
};
}
}Для URI-версіювання та глобального префікса цей маршрут буде доступний без версії:
GET /api/healthНейтральний маршрут не потрібно дублювати для кожної версії. Його слід використовувати лише тоді, коли контракт справді однаковий для всіх версій.
Приклад:
/api/v1/orders
/api/v2/ordersПереваги:
версію легко побачити;
просто тестувати в браузері та через curl;
зручно документувати;
легко маршрутизувати запити на різні реалізації.
Недолік — для кожної версії з’являється окремий URI.
Приклад:
X-API-Version: 2Переваги:
URI не змінюється;
версія є частиною метаданих запиту.
Недоліки:
версію не видно в адресі;
потрібно не забувати додавати заголовок;
кешування та ручне тестування можуть бути менш очевидними.
Приклад:
Accept: application/json;v=2Перевага — версія пов’язана з форматом відповіді. Недолік — синтаксис складніший для клієнтів і розробників.
Для більшості прикладних REST API в NestJS зручно починати з URI-версіювання. Головне — використовувати одну стратегію послідовно в межах API.
Під час створення нової версії не обов’язково дублювати весь застосунок. Зазвичай процес виглядає так:
Залишити реалізацію версії 1 без змін.
Додати новий метод або контролер для версії 2.
Перенести клієнтів на версію 2.
Підтримувати версію 1 протягом визначеного перехідного періоду.
Видалити версію 1 лише після завершення міграції клієнтів.
Якщо зміна не порушує старий контракт, нова версія може бути непотрібною. Наприклад, додавання необов’язкового поля у відповідь часто не вимагає створення нової версії. Натомість перейменування або видалення поля вже може бути несумісною зміною.
Якщо використовується URI-версіювання, потрібно додавати версію до адреси:
/api/v1/ordersа не:
/api/ordersВинятком є маршрут із VERSION_NEUTRAL.
Два методи з однаковими HTTP-методом, шляхом і версією створюють конфлікт. Наприклад, не слід мати два методи:
@Get()
@Version('1')
firstHandler() {}
@Get()
@Version('1')
secondHandler() {}Для однієї комбінації метод + шлях + версія має бути одна реалізація.
Саме ввімкнення версіювання не створює версію для кожного маршруту автоматично. Маршрут повинен мати версію на рівні контролера або методу:
@Controller({
path: 'orders',
version: '1',
})або:
@Get()
@Version('1')Якщо версії повертають різні структури даних, краще мати окремі методи або контролери. Не варто додавати багато умов на кшталт перевірки версії всередині одного методу, якщо це ускладнює підтримку.
Потрібно заздалегідь обрати формат версій і дотримуватися його. Наприклад:
1
2
3або:
2024-01
2024-06Не слід без потреби змішувати числові та довільні формати в одному API.
NestJS підтримує версіювання через URI, HTTP-заголовок і media type.
Версіювання вмикається методом app.enableVersioning().
Версію можна призначити контролеру через властивість version.
Для окремого маршруту використовується декоратор @Version().
Масив версій дає змогу використовувати одну реалізацію для кількох сумісних версій.
VERSION_NEUTRAL створює маршрут, незалежний від версії.
URI-версіювання зазвичай найпростіше тестувати та документувати.
Несумісні формати відповідей краще реалізовувати як окремі версії маршрутів.