Стратегии версионирования, обратная совместимость
Версионирование позволяет изменять API, сохраняя обратную совместимость. В этой теме вы изучите стратегии версионирования, управление изменениями и лучшие практики.
Версионирование API — механизм управления изменениями, позволяющий клиентам продолжать работать со старой версией, пока они не обновятся.
| Изменение | Тип | Версия |
|---|---|---|
| Добавление нового поля | Обратное | Не требуется |
| Добавление нового endpoint | Обратное | Не требуется |
| Удаление поля | Обратное | Требуется |
| Изменение типа поля | Обратное | Требуется |
| Изменение формата ответа | Обратное | Требуется |
| Исправление бага | Обратное | Не требуется |
Наиболее распространённый подход.
from fastapi import FastAPI, APIRouter
app = FastAPI()
# Версия 1
router_v1 = APIRouter(prefix='/api/v1')
@router_v1.get('/users')
def get_users_v1():
return [{'id': 1, 'name': 'John'}]
# Версия 2
router_v2 = APIRouter(prefix='/api/v2')
@router_v2.get('/users')
def get_users_v2():
return [
{
'id': 1,
'name': 'John',
'email': 'john@example.com' # Новое поле
}
]
app.include_router(router_v1)
app.include_router(router_v2)Преимущества:
Недостатки:
from fastapi import FastAPI, Request, Header
app = FastAPI()
@app.get('/users')
def get_users(api_version: str = Header('v1')):
if api_version == 'v1':
return [{'id': 1, 'name': 'John'}]
elif api_version == 'v2':
return [
{'id': 1, 'name': 'John', 'email': 'john@example.com'}
]
else:
raise HTTPException(400, "Invalid API version")Преимущества:
Недостатки:
from fastapi import FastAPI, Query
app = FastAPI()
@app.get('/users')
def get_users(version: str = Query('v1')):
if version == 'v1':
return [{'id': 1, 'name': 'John'}]
elif version == 'v2':
return [
{'id': 1, 'name': 'John', 'email': 'john@example.com'}
]Преимущества:
Недостатки:
from fastapi import FastAPI, Request
app = FastAPI()
@app.get('/users')
async def get_users(request: Request):
accept = request.headers.get('Accept', 'application/json')
if 'application/vnd.myapp.v1+json' in accept:
return [{'id': 1, 'name': 'John'}]
elif 'application/vnd.myapp.v2+json' in accept:
return [
{'id': 1, 'name': 'John', 'email': 'john@example.com'}
]Преимущества:
Недостатки:
project/
├── api/
│ ├── __init__.py
│ ├── v1/
│ │ ├── __init__.py
│ │ ├── users.py
│ │ └── posts.py
│ └── v2/
│ ├── __init__.py
│ ├── users.py
│ └── posts.py
└── main.py
from fastapi import APIRouter
router = APIRouter()
@router.get('/users')
def get_users():
return [
{'id': 1, 'name': 'John'},
{'id': 2, 'name': 'Jane'}
]
@router.get('/users/{user_id}')
def get_user(user_id: int):
return {'id': user_id, 'name': f'User {user_id}'}from fastapi import APIRouter
router = APIRouter()
@router.get('/users')
def get_users():
return [
{
'id': 1,
'name': 'John',
'email': 'john@example.com',
'role': 'user'
},
{
'id': 2,
'name': 'Jane',
'email': 'jane@example.com',
'role': 'admin'
}
]
@router.get('/users/{user_id}')
def get_user(user_id: int):
return {
'id': user_id,
'name': f'User {user_id}',
'email': f'user{user_id}@example.com',
'role': 'user'
}from fastapi import FastAPI
from api.v1 import users as users_v1
from api.v2 import users as users_v2
app = FastAPI(
title="My API",
description="API с версионированием",
version="2.0.0"
)
# Версия 1
app.include_router(users_v1.router, prefix='/api/v1', tags=['v1'])
# Версия 2
app.include_router(users_v2.router, prefix='/api/v2', tags=['v2'])
@app.get('/')
def root():
return {
'message': 'API с версионированием',
'versions': {
'v1': '/api/v1',
'v2': '/api/v2'
}
}# models/common.py
from pydantic import BaseModel
class UserBase(BaseModel):
name: str
# models/v1.py
from pydantic import BaseModel
from .common import UserBase
class UserResponse(UserBase):
id: int
class Config:
from_attributes = True
# models/v2.py
from pydantic import BaseModel, EmailStr
from .common import UserBase
class UserResponse(UserBase):
id: int
email: EmailStr
role: str = 'user'
class Config:
from_attributes = True# api/v1/users.py
from fastapi import APIRouter
from models.v1 import UserResponse
router = APIRouter()
@router.get('/users', response_model=list[UserResponse])
def get_users():
...
# api/v2/users.py
from fastapi import APIRouter
from models.v2 import UserResponse
router = APIRouter()
@router.get('/users', response_model=list[UserResponse])
def get_users():
...Помечайте устаревшие endpoints для клиентов.
from fastapi import FastAPI, status
app = FastAPI()
@app.get(
'/users',
deprecated=True,
summary="Устаревший endpoint",
description="Используйте /api/v2/users вместо этого"
)
def get_users_deprecated():
return [{'id': 1, 'name': 'John'}]
@app.get('/api/v2/users')
def get_users_v2():
return [
{'id': 1, 'name': 'John', 'email': 'john@example.com'}
]В Swagger UI устаревший endpoint будет зачёркнут.
from fastapi import FastAPI
from fastapi.openapi.docs import get_swagger_ui_html
app = FastAPI(docs_url=None, redoc_url=None)
# Версия 1
app_v1 = FastAPI(docs_url='/docs', redoc_url='/redoc')
@app.get('/docs', include_in_schema=False)
async def custom_swagger_ui_html():
return get_swagger_ui_html(
openapi_url="/openapi_v1.json",
title="API v1 Docs"
)app = FastAPI()
app.include_router(
users_v1.router,
prefix='/api/v1',
tags=['Users v1']
)
app.include_router(
users_v2.router,
prefix='/api/v2',
tags=['Users v2']
)# v1
class UserResponse(BaseModel):
id: int
name: str
# v2 (добавлено новое поле)
class UserResponse(BaseModel):
id: int
name: str
email: str # Новое, необязательное поле
# Обратное: старые клиенты продолжат работать# v1
class UserResponse(BaseModel):
id: int
name: str
deprecated_field: str # Устаревшее
# v2 (удалено поле)
class UserResponse(BaseModel):
id: int
name: str
# Не обратное: старые клиенты сломаются
# Решение: пометить deprecated в v1, удалить в v3# v1
class UserResponse(BaseModel):
age: int # Число
# v2 (изменён тип)
class UserResponse(BaseModel):
age: str # Строка "25 years"
# Не обратное: старые клиенты сломаются
# Решение: добавить новое поле
class UserResponse(BaseModel):
age: int
age_display: str # Новое полеfrom fastapi import FastAPI, APIRouter, Depends, HTTPException
from pydantic import BaseModel, EmailStr, Field
from typing import Optional
app = FastAPI(title="My API", version="2.0.0")
# === Models v1 ===
class UserResponseV1(BaseModel):
id: int
name: str
class Config:
from_attributes = True
class UserCreateV1(BaseModel):
name: str = Field(..., min_length=3)
# === Models v2 ===
class UserResponseV2(BaseModel):
id: int
name: str
email: EmailStr
role: str = 'user'
class Config:
from_attributes = True
class UserCreateV2(BaseModel):
name: str = Field(..., min_length=3)
email: EmailStr
role: str = 'user'
# === Имитация БД ===
users_db = {
1: {'id': 1, 'name': 'John', 'email': 'john@example.com', 'role': 'user'},
2: {'id': 2, 'name': 'Jane', 'email': 'jane@example.com', 'role': 'admin'},
}
# === API v1 ===
router_v1 = APIRouter(prefix='/api/v1', tags=['Users v1'])
@router_v1.get('/users', response_model=list[UserResponseV1])
def get_users_v1():
"""Получить список пользователей (v1)"""
return list(users_db.values())
@router_v1.get('/users/{user_id}', response_model=UserResponseV1)
def get_user_v1(user_id: int):
"""Получить пользователя по ID (v1)"""
if user_id not in users_db:
raise HTTPException(404, "User not found")
return users_db[user_id]
@router_v1.post('/users', response_model=UserResponseV1, status_code=201)
def create_user_v1(user: UserCreateV1):
"""Создать пользователя (v1)"""
user_id = max(users_db.keys()) + 1
user_data = {'id': user_id, 'name': user.name}
users_db[user_id] = user_data
return user_data
app.include_router(router_v1)
# === API v2 ===
router_v2 = APIRouter(prefix='/api/v2', tags=['Users v2'])
@router_v2.get('/users', response_model=list[UserResponseV2])
def get_users_v2():
"""Получить список пользователей (v2)"""
return list(users_db.values())
@router_v2.get('/users/{user_id}', response_model=UserResponseV2)
def get_user_v2(user_id: int):
"""Получить пользователя по ID (v2)"""
if user_id not in users_db:
raise HTTPException(404, "User not found")
return users_db[user_id]
@router_v2.post('/users', response_model=UserResponseV2, status_code=201)
def create_user_v2(user: UserCreateV2):
"""Создать пользователя (v2)"""
user_id = max(users_db.keys()) + 1
user_data = {
'id': user_id,
'name': user.name,
'email': user.email,
'role': user.role
}
users_db[user_id] = user_data
return user_data
app.include_router(router_v2)
# === Deprecated endpoint ===
@app.get('/users', deprecated=True, tags=['Deprecated'])
def get_users_deprecated():
"""
Устаревший endpoint.
Используйте /api/v1/users или /api/v2/users
"""
return list(users_db.values())
# === Информация о версиях ===
@app.get('/versions')
def get_versions():
"""
Информация о доступных версиях API.
"""
return {
'versions': {
'v1': {
'url': '/api/v1',
'status': 'stable',
'deprecated': False
},
'v2': {
'url': '/api/v2',
'status': 'stable',
'deprecated': False
}
},
'current_version': 'v2'
}from fastapi import Response
@app.get('/users')
def get_users(response: Response):
response.headers['Deprecation'] = 'true'
response.headers['Sunset'] = '2024-12-31' # Дата удаления
return list(users_db.values())@app.get('/changelog')
def get_changelog():
return {
'v2.0.0': {
'date': '2024-01-01',
'changes': [
'Добавлено поле email в ответе пользователя',
'Добавлено поле role',
'Исправлена пагинация'
]
},
'v1.0.0': {
'date': '2023-01-01',
'changes': [
'Первый релиз API'
]
}
}# Было
class UserResponse(BaseModel):
name: str
# Стало (сломало клиентов)
class UserResponse(BaseModel):
full_name: str # Изменили имя поляРешение: Создайте новую версию API.
# Не поддерживайте v1, v2, v3, v4, v5 одновременноРешение: Установите политику: поддерживать последние 2 версии, устаревшие удалять через 6 месяцев.
Решение: Ведите changelog, уведомляйте клиентов о deprecated заранее.
В следующей теме вы изучите документирование API — OpenAPI, Swagger, ReDoc, кастомизация документации.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.