← Назад к вопросам

Что будет если backend изменил какое-то свойство?

3.0 Senior🔥 131 комментариев
#DevOps и инфраструктура#Django

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Что случится, если backend изменил свойство?

Это критический вопрос о API контракте и обратной совместимости. Такие изменения могут сломать фронтенд, если не подойти к этому продуманно. Расскажу о проблеме и как её решать.

Сценарий: что ломается

# Backend: Old API endpoint
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {
        "id": user_id,
        "name": "John",
        "email": "john@example.com",
        "age": 25  # ← Это свойство
    }

# Frontend зависит от этого свойства
function renderUser(data) {
    return `${data.name}, ${data.age} лет`  // ← Используется age
}

# Backend меняет структуру
# ❌ Backend убирает age
@app.get("/users/{user_id}")
async def get_user(user_id: int):
    return {
        "id": user_id,
        "name": "John",
        "email": "john@example.com"
        # age удалён
    }

# Результат на фронте
// console: ${data.name}, undefined лет  ← Ошибка!

Проблема: версионирование API

# ❌ Плохо — нет версионирования
@app.get("/users/{user_id}")  # Любое изменение ломает клиентов
async def get_user(user_id: int):
    return {...}

# ✅ Хорошо — с версионированием
@app.get("/api/v1/users/{user_id}")  # V1 сохраняется вечно
async def get_user_v1(user_id: int):
    return {"id": ..., "name": ..., "age": ...}

@app.get("/api/v2/users/{user_id}")  # V2 может изменяться
async def get_user_v2(user_id: int):
    return {"id": ..., "name": ...}  # age удалён

Решение 1: Семантическое версионирование

Используй правило Major.Minor.Patch:

  • Major (3.0.0) — breaking changes (удаление полей)
  • Minor (3.1.0) — добавление полей (обратно совместимо)
  • Patch (3.1.1) — баг фиксы
# Backend версионирование
class UserV1(BaseModel):
    id: int
    name: str
    email: str
    age: int  # ← Есть в V1

class UserV2(BaseModel):
    id: int
    name: str
    email: str
    # age удалён в V2

@app.get("/api/v1/users/{user_id}", response_model=UserV1)
async def get_user_v1(user_id: int):
    user = await db.get_user(user_id)
    return UserV1(**user.dict())

@app.get("/api/v2/users/{user_id}", response_model=UserV2)
async def get_user_v2(user_id: int):
    user = await db.get_user(user_id)
    return UserV2(**user.dict())

Решение 2: Graceful Degradation на фронте

// Frontend: не полагайся на конкретные поля

// ❌ Плохо — зависит от существования поля
function renderUser(data) {
    return `${data.name}, ${data.age} лет`
}

// ✅ Хорошо — обработай отсутствие поля
function renderUser(data) {
    const ageText = data.age ? `, ${data.age} лет` : ''
    return `${data.name}${ageText}`
}

// ✅ Ещё лучше — типизируй и валидируй
interface User {
    id: number
    name: string
    email: string
    age?: number  // ← Optional
}

function renderUser(data: Partial<User>) {
    const ageText = data.age ? `, ${data.age} лет` : ''
    return `${data.name ?? 'Unknown'}${ageText}`
}

Решение 3: Контракт-тестирование (Consumer-Driven Contracts)

# Backend: явно документирует контракт
from enum import Enum

class UserResponse(BaseModel):
    """Контракт API V1"""
    id: int
    name: str
    email: str
    age: int
    
    class Config:
        json_schema_extra = {
            "example": {
                "id": 1,
                "name": "John",
                "email": "john@example.com",
                "age": 25
            }
        }

# Frontend может генерировать типы из этого контракта
# (OpenAPI, Protocol Buffers, GraphQL schema)

# Тест на совместимость
def test_user_response_contract():
    """Гарантирует, что ответ соответствует контракту"""
    response = get_user_v1(user_id=1)
    
    # Проверяем обязательные поля
    assert "id" in response
    assert "name" in response
    assert "email" in response
    assert "age" in response  # ← Это отловит, если уберут поле

Решение 4: Deprecation policy

# Backend: показывает, что поле скоро удалится
from typing import Optional
import warnings

class UserResponse(BaseModel):
    id: int
    name: str
    email: str
    age: Optional[int] = None  # ← Deprecated
    
    @field_validator('age')
    @classmethod
    def validate_age(cls, v):
        warnings.warn(
            "Field 'age' is deprecated and will be removed in v3. "
            "Use 'dateOfBirth' instead.",
            DeprecationWarning,
            stacklevel=2
        )
        return v

@app.get("/api/v2/users/{user_id}")
async def get_user_v2(user_id: int):
    # Эндпоинт V2 существует, но возвращает оба поля
    user = await db.get_user(user_id)
    return UserResponse(
        id=user.id,
        name=user.name,
        email=user.email,
        age=user.age  # ← Для обратной совместимости
    )

Решение 5: Additive changes only (минимизируй breaking changes)

# ✅ Правило: добавляй, не удаляй

# Было в V1
class UserV1(BaseModel):
    id: int
    name: str
    email: str
    age: int

# Стало в V2 — добавили поля, не удалили
class UserV2(BaseModel):
    id: int
    name: str
    email: str
    age: int  # ← Сохранили для совместимости
    dateOfBirth: str  # ← Добавили новое
    avatar: Optional[str] = None  # ← С дефолтом

# Frontend может переходить на новые поля когда готов
# Старый код продолжает работать

Реальный пример: миграция с версионирования

# Шаг 1: Поддерживаем обе версии
@app.get("/api/v1/users/{user_id}")  # Старые клиенты
async def get_user_v1(user_id: int):
    return UserV1(...)

@app.get("/api/v2/users/{user_id}")  # Новые клиенты
async def get_user_v2(user_id: int):
    return UserV2(...)

# Шаг 2: Информируем клиентов об устаревании
# (отправляем письма, документируем)

# Шаг 3: Плановое удаление V1 (через 6-12 месяцев)
# /api/v1/* возвращает 410 Gone с инструкциями

@app.get("/api/v1/users/{user_id}")
async def get_user_v1_deprecated(user_id: int):
    raise HTTPException(
        status_code=410,
        detail="API v1 is deprecated. Please migrate to v2. See https://docs.example.com/migration"
    )

Best Practices

1. Всегда версионируй API (/api/v1/, /api/v2/)
2. Поддерживай минимум 2 версии одновременно
3. Документируй контракт явно (OpenAPI, Swagger)
4. Обучай фронтенд обрабатывать missing fields
5. Используй optional и default значения
6. Предупреждай за 6 месяцев о breaking changes
7. Пиши контракт-тесты
8. Избегай удаления полей (лучше deprecate)

Заключение

Изменение свойства backend-а может сломать фронтенд. Это решается через API версионирование, явный контракт, graceful degradation на клиенте и продуманную стратегию миграции. Это не проблема единственного разработчика, это ответственность команды и архитектуры системы.