API versioning: стратегия и deprecation
URL path vs Accept header vs media-type, deprecation timeline, sunset headers, как не застрять на v2 навсегда.
Спроектируй стратегию versioning для API: {{api_surface}}. Потребители: {{consumers}}.
Базовая аксиома: versioning — это контракт с будущим. Каждая новая версия — налог: документация, поддержка, миграции. Цель — менять API так, чтобы новой версии не понадобилось годами. Если у тебя уже есть v2, v3, v4 — это симптом, а не достижение.
1. Когда versioning вообще нужен
Не нужен:
- Внутренний сервис, один consumer которого ты контролируешь — деплоишь оба вместе
- API в alpha/beta с предупреждением "контракт может меняться"
Нужен:
- Public API (third-party разработчики)
- Mobile clients (старые версии не обновляются месяцами)
- Partner integrations (контракты, SLA)
2. Где жить версии — три варианта
| Подход | Пример | Плюсы | Минусы |
|---|---|---|---|
| URL path | /v1/users/123 | Видно в логах/curl, легко роутить в gateway | Версионируется весь API, не отдельные ресурсы |
| Accept header | Accept: application/vnd.api.v2+json | Идеологически чисто (URL = ресурс) | Невидимо в браузере, ломает кэширование, тяжело отлаживать |
| Query param | ?version=2 | Просто | Считается анти-паттерном, путает кэширование |
Рекомендация по умолчанию: URL path (/v1/). 95% public API так делают (Stripe, GitHub, Twilio). Accept header — только если у тебя HATEOAS-like дизайн.
Никогда: version в body ({"version": "2", ...}) — это уже не versioning, а маршалинг.
3. Что считать breaking change
Breaking (требует новой версии):
- Удаление поля, endpoint'а, query параметра
- Переименование (path, поле, enum value)
- Изменение типа поля (string → number, optional → required)
- Изменение семантики (раньше 404 для несуществующих → теперь 200 с empty)
- Ужесточение валидации (раньше принимали emoji в имени → теперь нет)
- Изменение HTTP статуса (200 → 201, 400 → 422)
- Изменение defaults
Non-breaking (можно в текущей версии):
- Добавление optional поля в response
- Добавление optional query/body параметра
- Добавление нового endpoint
- Добавление enum value (опасно — клиенты могут не знать о новом)
- Расширение валидации (раньше max 100 символов → теперь 200)
Серая зона: добавление enum value. Если клиент делает switch(status) без default — сломаешь. Документируй как "may add new values; handle unknown gracefully".
4. Deprecation timeline
Правило: минимум 6 месяцев между объявлением и удалением. Для public API — 12+ месяцев.
T+0 ── Анонс депрекации (changelog, email partners, banner в docs)
T+1м ── Sunset header в каждом response: Sunset: Sat, 31 May 2025 23:59:59 GMT
T+3м ── Warning email тем, кто всё ещё использует v1 (нужны usage metrics)
T+5м ── Brownout: рандомные 503 на 1 час раз в неделю (форсит миграцию)
T+6м+ ── Удаление, redirect на error page с инструкцией
Sunset header — RFC 8594:
HTTP/1.1 200 OK
Sunset: Sat, 31 May 2025 23:59:59 GMT
Deprecation: true
Link: <https://docs.api.com/migrate-v1-to-v2>; rel="sunset"
5. Версионирование на уровне ресурсов (когда URL не годится)
Иногда нужно: добавить новое поле формата, но старые клиенты не понимают новый формат. Варианты:
# Content negotiation per resource
GET /users/123
Accept: application/vnd.api.user.v2+json
Или field-level versioning:
{
"id": 123,
"name": "Alice",
"address": "Old format string",
"address_v2": { "street": "...", "city": "..." }
}
Уродливо, но позволяет не плодить v2 всего API ради одного поля. Удалить address через deprecation.
6. Как не дойти до v2
Большинство breaking changes можно избежать дизайном:
- Никогда не возвращай array на root —
["a", "b"]нельзя расширить. Возвращай{"items": [...], "next_cursor": null} - Никогда не возвращай примитив —
trueнельзя расширить. Возвращай{"ok": true} - Booleans → enums —
{"approved": true}нельзя расширить до состояний "pending/approved/rejected". Возвращай{"status": "approved"} - IDs всегда string — позволяет менять формат (int64 → UUID) без breaking
- Дата всегда ISO 8601 string — не Unix timestamp number
- Пагинация — cursor, не offset — позволяет менять backend без ломания пагинации
- Default
additionalProperties: falseв схеме — клиенты не должны полагаться на неописанные поля
7. Поддержка нескольких версий в коде
Три подхода:
Параллельные пути:
/v1/users.controller.ts
/v2/users.controller.ts
Просто, но дублирование. Подходит когда v2 — большой передизайн.
Адаптеры:
// Single internal model, version-specific (de)serializers
v1Adapter.serialize(user) // → old format
v2Adapter.serialize(user) // → new format
Меньше дублирования, но адаптеры разрастаются.
Feature flags per version:
if (req.apiVersion >= 2) {
response.address = structuredAddress;
} else {
response.address = formatLegacyAddress(structuredAddress);
}
Опасно — версии переплетаются в одном файле. Только для маленьких различий.
8. Usage tracking — обязательно
Без usage metrics deprecation невозможна. Логируй на каждом запросе:
- Версия API
- Consumer (API key / OAuth client_id)
- Endpoint
- User agent (для mobile — версия приложения)
Dashboard: "сколько % трафика всё ещё на v1, кто эти клиенты, когда последний раз слышали от них". Без этого ты не знаешь когда удалять.
9. Что отдать на выходе
- Решение: URL path vs Accept header (с обоснованием для {{consumers}})
- Breaking change policy (что breaking, что нет)
- Deprecation timeline (X месяцев, какие шаги)
- Sunset/Deprecation headers формат
- Design rules чтобы не дойти до v2 (списки выше)
- Usage tracking план
Anti-patterns
- ❌ Версия в каждом эндпоинте без единой стратегии (
/users/v2,/api/v3/posts,?ver=4) — нечитаемый зоопарк - ❌ Удаление endpoint'а без deprecation period — partner integrations падают, ты теряешь доверие
- ❌ Bump major version ради добавления optional поля — нагружает всех консьюмеров миграцией без причины
- ❌ Versioning только в gateway, internal сервисы не знают версию — нельзя сделать version-specific логику
- ❌ Sunset header написан, но usage не трекается — удалишь то что ещё используется, или будешь поддерживать v1 вечно
- ❌ v2 как "v1 + куча новых фич" — заставляет мигрировать ради фич, миграция big-bang
- ❌ Версионирование без changelog с примерами
до/после— клиенты не знают что менять - ❌ Поддержка 5+ версий одновременно — комбинаторный взрыв тестов и багов
В конце
- Стратегия (URL vs header) + обоснование
- Breaking change classification документ
- Deprecation timeline (6/12 месяцев) + sunset header schema
- Design guidelines (objects, не arrays; enums, не booleans; cursors, не offsets)
- Usage tracking dashboard
- Owners: кто решает «делаем v2», кто отвечает за миграцию консьюмеров
Что такое API простыми словами + примеры
Все говорят «вызови API», «у этого сервиса есть API» — а что это? Объясним через ресторан и официанта. Плюс реальный запрос.
Интеграция стороннего сервиса
План подключения сервиса (Stripe, Supabase, etc.) с учётом ошибок, секретов и тестового режима.
Проектирование REST/RPC API
Ресурсы, эндпойнты, контракты, версионирование, ошибки, идемпотентность, rate limits.