Привет! API развиваются. Новые поля, изменённые форматы, удалённые эндпоинты — всё это может сломать старых клиентов. Версионирование API — это способ вводить изменения, не ломая существующие интеграции.
В этой статье мы разберём:
- Что такое версионирование API
- Основные стратегии: URL path, query parameter, header, media type
- Что считать breaking change
- Как управлять несколькими версиями
- Практические примеры в FastAPI
- Что нужно знать перед началом
- Основная часть
- Что такое версионирование API
- Стратегия 1: URL Path Versioning
- Стратегия 2: Query Parameter Versioning
- Стратегия 3: Header Versioning
- Стратегия 4: Media Type Versioning (Content Negotiation)
- Сравнение стратегий
- Управление несколькими версиями
- Депрекейтация и sunset
- Полный пример
- Задачи для закрепления
- Нюансы и подводные камни
- Версионирование — это не про код, а про контракт
- Не версионируй minor-версии
- Версионирование не заменяет обратную совместимость
- Частые ошибки и как их избежать
- Ошибка 1: Версионирование каждой мелочи
- Ошибка 2: Удаление старой версии без предупреждения
- Ошибка 3: Отсутствие документации по версиям
- Шпаргалка
- Заключение
- КВИЗ
- Что дальше?
Что нужно знать перед началом
Для этого урока тебе понадобится:
- Установленный 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 12 | Sunset — удалить версию (возвращать 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 |
| Header | API-Version: 1 |
| Media Type | Accept: application/vnd.myapi.v1+json |
Заключение
Сегодня мы:
- Узнали, что такое версионирование API
- Изучили 4 основные стратегии
- Сравнили их плюсы и минусы
- Реализовали примеры в FastAPI








