Версионирование API: стратегии и примеры

Версионирование API: стратегии и примеры REST API

Привет! API развиваются. Новые поля, изменённые форматы, удалённые эндпоинты — всё это может сломать старых клиентов. Версионирование API — это способ вводить изменения, не ломая существующие интеграции.

В этой статье мы разберём:

  • Что такое версионирование API
  • Основные стратегии: URL path, query parameter, header, media type
  • Что считать breaking change
  • Как управлять несколькими версиями
  • Практические примеры в FastAPI

Что нужно знать перед началом

Для этого урока тебе понадобится:

  • Установленный FastAPI
  • Базовое понимание эндпоинтов и маршрутизации

Совет: Версионирование — это не про «как сделать красиво», а про «как не сломать клиентов».

Основная часть

Что такое версионирование API

Версионирование API — это практика управления изменениями в API, при которой клиенты могут продолжать использовать старые версии, пока не перейдут на новые.

Когда нужна новая версия?

Breaking change — это изменение, которое требует от клиента обновить код:

Что ломаетЧто НЕ ломает
Удаление или переименование поляДобавление опционального поля
Изменение типа данныхДобавление нового эндпоинта
Удаление эндпоинтаДобавление query-параметра
Изменение статус-кода ошибкиИзменение внутренней логики
Обязательное поле вместо опциональногоУлучшение производительности

Совет: Золотое правило — старые клиенты должны продолжать работать без изменений.

Стратегия 1: URL Path Versioning

Самый популярный способ — версия в URL.

# v1 и v2 — разные эндпоинты
GET /v1/users
GET /v2/users

Пример в FastAPI:

from fastapi import FastAPI

app = FastAPI()

# v1 — старый формат
@app.get("/v1/users/{user_id}")
def get_user_v1(user_id: int):
    return {"id": user_id, "name": f"User {user_id}"}

# v2 — новый формат (с email)
@app.get("/v2/users/{user_id}")
def get_user_v2(user_id: int):
    return {"id": user_id, "name": f"User {user_id}", "email": f"user{user_id}@example.com"}

Плюсы: 

  • Версия видна в URL, легко тестировать
  • Работает с любыми HTTP-инструментами
  • Кеширование по умолчанию (разные URL → разные кеши)
  • Простая депрекейтация (удалить /v1)

Минусы: 

  • «Загрязнение» URL-пространства
  • Не «чистый» REST (ресурс должен иметь один URL)

Стратегия 2: Query Parameter Versioning

Версия передаётся как query-параметр.

GET /users?version=1
GET /users?version=2

Пример в FastAPI:

from fastapi import FastAPI, Query

app = FastAPI()

@app.get("/users")
def get_users(version: int = Query(1, ge=1, le=3)):
    if version == 1:
        return [{"id": 1, "name": "Anna"}]
    elif version == 2:
        return [{"id": 1, "name": "Anna", "email": "anna@example.com"}]
    else:
        return [{"id": 1, "name": "Anna", "email": "anna@example.com", "age": 25}]

Плюсы: 

  • URL остаётся чистым
  • Легко задать версию по умолчанию

Минусы: 

  • Может конфликтовать с кешированием (старые прокси не кешируют query-параметры)
  • Версия скрыта в строке запроса

Стратегия 3: Header Versioning

Версия передаётся в HTTP-заголовке.

GET /users
API-Version: 1

Пример в FastAPI:

from fastapi import FastAPI, Header, HTTPException

app = FastAPI()

@app.get("/users")
def get_users(api_version: int = Header(1)):
    if api_version == 1:
        return [{"id": 1, "name": "Anna"}]
    elif api_version == 2:
        return [{"id": 1, "name": "Anna", "email": "anna@example.com"}]
    else:
        raise HTTPException(status_code=400, detail="Unsupported version")

Плюсы: 

  • Чистые URL
  • Более «RESTful» (ресурс не меняется)

Минусы: 

  • Версия не видна в браузере/логах
  • Сложнее тестировать (нужно добавлять заголовок)
  • Кеширование требует Vary: API-Version

Стратегия 4: Media Type Versioning (Content Negotiation)

Версия указывается в Accept заголовке с кастомным media type.

GET /users
Accept: application/vnd.myapi.v1+json

Пример в FastAPI:

from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse

app = FastAPI()

@app.get("/users")
async def get_users(request: Request):
    accept = request.headers.get("accept", "")
    
    if "vnd.myapi.v1" in accept:
        return [{"id": 1, "name": "Anna"}]
    elif "vnd.myapi.v2" in accept:
        return [{"id": 1, "name": "Anna", "email": "anna@example.com"}]
    else:
        # default to latest
        return [{"id": 1, "name": "Anna", "email": "anna@example.com", "age": 25}]

Плюсы: 

  • Самый «RESTful» подход (по стандарту HTTP)
  • Поддерживает разные форматы (JSON, XML) одновременно

Минусы: 

  • Сложная реализация
  • Плохая поддержка инструментами
  • Очень сложно тестировать

Сравнение стратегий

СтратегияВидимостьСложностьКешированиеRESTful
URL PathВысокаяНизкаяПростоеСредняя
Query ParamСредняяНизкаяСложноеНизкая
HeaderНизкаяСредняяСложноеВысокая
Media TypeОчень низкаяВысокаяОчень сложноеВысокая

Совет: Для большинства проектов выбирай URL Path Versioning — он самый простой и понятный .

Управление несколькими версиями

Структура проекта:

app/
├── v1/
│   ├── __init__.py
│   ├── models.py
│   ├── schemas.py
│   └── routers.py
├── v2/
│   ├── __init__.py
│   ├── models.py
│   ├── schemas.py
│   └── routers.py
└── main.py

Пример с FastAPI APIRouter:

# app/v1/routers.py
from fastapi import APIRouter

router = APIRouter(prefix="/v1", tags=["v1"])

@router.get("/users")
def get_users():
    return [{"id": 1, "name": "Anna"}]

# app/v2/routers.py
from fastapi import APIRouter

router = APIRouter(prefix="/v2", tags=["v2"])

@router.get("/users")
def get_users():
    return [{"id": 1, "name": "Anna", "email": "anna@example.com"}]

# main.py
from fastapi import FastAPI
from app.v1 import routers as v1_routers
from app.v2 import routers as v2_routers

app = FastAPI()
app.include_router(v1_routers.router)
app.include_router(v2_routers.router)

Преимущества:

  • Чистое разделение версий
  • Общая бизнес-логика может быть вынесена в отдельные модули
  • Легко добавлять новые версии
  • Легко удалять старые

Депрекейтация и sunset

Процесс депрекейтации: 

ЭтапДействие
Month 0Объявить депрекейтацию, добавить Deprecation заголовок
Month 3Уведомить клиентов по email
Month 6Добавить предупреждение в документацию
Month 9Снизить rate limit для старой версии
Month 12Sunset — удалить версию (возвращать 410 Gone)

Пример с заголовками депрекейтации:

from fastapi import FastAPI, Response

app = FastAPI()

@app.get("/v1/users")
def get_users_v1(response: Response):
    # RFC 8594: Sunset header
    response.headers["Sunset"] = "2026-12-31"
    response.headers["Deprecation"] = "true"
    response.headers["Link"] = '<https://api.example.com/docs/migration>; rel="deprecation"; type="text/html"'
    
    return [{"id": 1, "name": "Anna (v1)"}]

Совет: Используй стандартные заголовки Deprecation и Sunset (RFC 8594, RFC 9745) для депрекейтации.

Полный пример

from fastapi import FastAPI, Header, HTTPException, Response
from datetime import date

app = FastAPI(title="Versioned API")

# ======== Версия 1 ========
@app.get("/v1/users")
def get_users_v1(response: Response):
    response.headers["Deprecation"] = "true"
    response.headers["Sunset"] = "2026-12-31"
    return {"version": 1, "users": [{"id": 1, "name": "Anna"}]}

# ======== Версия 2 ========
@app.get("/v2/users")
def get_users_v2():
    return {"version": 2, "users": [{"id": 1, "name": "Anna", "email": "anna@example.com"}]}

# ======== Поддержка нескольких версий через один эндпоинт ========
@app.get("/api/users")
def get_users(api_version: int = Header(1)):
    if api_version == 1:
        return {"version": 1, "users": [{"id": 1, "name": "Anna"}]}
    elif api_version == 2:
        return {"version": 2, "users": [{"id": 1, "name": "Anna", "email": "anna@example.com"}]}
    else:
        raise HTTPException(status_code=400, detail="Unsupported version")

# ======== Согласование версий через Accept ========
@app.get("/api/products")
async def get_products(request: Request):
    accept = request.headers.get("accept", "")
    
    if "vnd.myapi.v1" in accept:
        return {"version": 1, "products": ["Product A"]}
    else:
        # default to v2
        return {"version": 2, "products": [{"name": "Product A", "price": 100}]}

Задачи для закрепления

Задача 1. Создай две версии эндпоинта /users: v1 с полями id, name, v2 с полями id, name, email.

Задача 2. Реализуй версионирование через заголовок API-Version.

Задача 3. Добавь заголовок Deprecation для v1.

Задача 4. Реализуй версионирование через Accept заголовок.

Задача 5. Раздели v1 и v2 на отдельные роутеры.

Ответы:

Задача 1. 

@app.get("/v1/users")
def get_users_v1():
    return [{"id": 1, "name": "Anna"}]

@app.get("/v2/users")
def get_users_v2():
    return [{"id": 1, "name": "Anna", "email": "anna@example.com"}]

Задача 2. 

@app.get("/users")
def get_users(api_version: int = Header(1)):
    if api_version == 1:
        return [{"id": 1, "name": "Anna"}]
    return [{"id": 1, "name": "Anna", "email": "anna@example.com"}]

Задача 3. 

@app.get("/v1/users")
def get_users_v1(response: Response):
    response.headers["Deprecation"] = "true"
    return [{"id": 1, "name": "Anna"}]

Задача 4. 

@app.get("/api/products")
async def get_products(request: Request):
    accept = request.headers.get("accept", "")
    if "vnd.myapi.v1" in accept:
        return {"version": 1, "items": ["A"]}
    return {"version": 2, "items": [{"name": "A", "price": 100}]}

Задача 5. 

# Создай папки v1/ и v2/ с routers.py, импортируй и используй include_router()

Нюансы и подводные камни

Версионирование — это не про код, а про контракт

Клиентам всё равно, как устроен ваш код. Им важно, чтобы API работало как раньше.

Не версионируй minor-версии

Клиентам нужны только major версии. Minor и patch — для внутреннего использования.

Версионирование не заменяет обратную совместимость

Старайся делать не-breaking изменения чаще, чем создавать новую версию.

Частые ошибки и как их избежать

Ошибка 1: Версионирование каждой мелочи

Новая версия — только для breaking changes.

Ошибка 2: Удаление старой версии без предупреждения

Всегда давай клиентам время на миграцию (минимум 6–12 месяцев).

Ошибка 3: Отсутствие документации по версиям

Документируй каждую версию и изменения между ними.

Шпаргалка

СтратегияКак пишется
URL Path/v1/users
Query Param?version=1
HeaderAPI-Version: 1
Media TypeAccept: application/vnd.myapi.v1+json

Заключение

Сегодня мы:

  • Узнали, что такое версионирование API
  • Изучили 4 основные стратегии
  • Сравнили их плюсы и минусы
  • Реализовали примеры в FastAPI

КВИЗ

Что дальше?

Оцените статью
IMI-DS - PYTHON LERNEN
Содержание
Оглавление ×