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

Как обработать ошибку обратной несовместимости изменений?

2.3 Middle🔥 121 комментариев
#API и сетевые протоколы#Архитектура и паттерны

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

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

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

Обработка ошибок обратной несовместимости

Это критически важная тема для production систем. Breaking changes — одна из самых опасных ошибок в разработке. Расскажу о всех уровнях защиты:

1. Профилактика — главный метод

Семантическое версионирование (semver):

  • MAJOR.MINOR.PATCH (например 2.3.1)
  • MAJOR — breaking changes
  • MINOR — новый функционал, backward compatible
  • PATCH — багфиксы

Правило: если меняешь API, это ВСЕГДА major version.

2. Стратегии в API дизайне

Депрекейшн вместо удаления:

// Старый endpoint
app.get('/users/:id', (req, res) => {
  // ДА: отправляем заголовок deprecation
  res.set('Deprecation', 'true');
  res.set('Sunset', 'Fri, 31 Dec 2024 23:59:59 GMT');
  res.set('Link', '</api/v2/users/:id>; rel="successor-version"');
  
  // Поддерживаем старый формат
  const user = getUser(req.params.id);
  res.json({
    id: user.id,
    name: user.name,
    email: user.email // Старое поле
  });
});

// Новый endpoint с другой структурой
app.get('/v2/users/:id', (req, res) => {
  const user = getUser(req.params.id);
  res.json({
    id: user.id,
    name: user.name,
    contactEmail: user.email // Переименовано
  });
});

3. Versioning стратегии

URL versioning (самый очевидный):

app.get('/api/v1/users', handleV1);
app.get('/api/v2/users', handleV2);

Header-based versioning:

app.get('/api/users', (req, res) => {
  const version = req.get('API-Version') || '1';
  if (version === '2') {
    return handleV2(req, res);
  }
  handleV1(req, res);
});

Accept header (Content negotiation):

app.get('/api/users', (req, res) => {
  const contentType = req.get('Accept');
  if (contentType.includes('application/vnd.myapi.v2+json')) {
    return res.json(v2Format);
  }
  res.json(v1Format);
});

4. Практические примеры

Сценарий 1: Переименование поля

// Миграционный период
function getUserResponse(user, apiVersion) {
  const response = {
    id: user.id,
    name: user.name
  };
  
  if (apiVersion === '1') {
    // Старые клиенты получают старые названия
    response.email = user.contactEmail;
  } else {
    // v2 — новое название
    response.contactEmail = user.contactEmail;
  }
  
  return response;
}

Сценарий 2: Изменение типа данных

// ПЛОХО: просто меняешь
res.json({ age: "25" }); // было число, теперь строка —破裂!

// ХОРОШО: добавляешь новое поле, старое сохраняешь
res.json({
  age: 25,           // v1 — число
  ageString: "25",  // v2 — строка
});

// ИДЕАЛЬНО: новый endpoint
// /api/v2/users всегда возвращает { age: "25" }

Сценарий 3: Новые обязательные параметры

// ПЛОХО: добавляешь обязательное поле
validate(body, ['id', 'name', 'newRequiredField']); // ломает старых клиентов

// ХОРОШО: опциональное с дефолтом
const newField = body.newField || getDefault();

5. Миграция клиентов

Roadmap с временной шкалой:

Month 1-2: Announce deprecation
- Добавь Deprecation header
- Отправь письма клиентам
- Создай документацию по миграции

Month 3-9: Transition period
- Поддерживай обе версии
- Собирай метрики (сколько клиентов ещё на v1)
- Помогай клиентам с миграцией

Month 10+: Sunset
- Отключи старый API
- Логируй попытки доступа

6. Мониторинг и метрики

// Отслеживание использования старых версий
app.use((req, res, next) => {
  if (req.path.includes('/api/v1/')) {
    metrics.increment('api.v1.requests');
    logger.warn('v1 API request', {
      endpoint: req.path,
      clientId: req.headers['x-client-id']
    });
  }
  next();
});

// Алерты если много requests к deprecated API
if (metrics.get('api.v1.requests') > threshold) {
  sendAlert('Too many v1 requests, some clients not migrated');
}

7. Тестирование

// Тесты для обеих версий
describe('User API', () => {
  it('v1 returns email field', async () => {
    const res = await request(app).get('/api/v1/users/1');
    expect(res.body).toHaveProperty('email');
  });
  
  it('v2 returns contactEmail field', async () => {
    const res = await request(app).get('/api/v2/users/1');
    expect(res.body).toHaveProperty('contactEmail');
  });
  
  it('v2 does not return email field', async () => {
    const res = await request(app).get('/api/v2/users/1');
    expect(res.body).not.toHaveProperty('email');
  });
});

8. Если уже случилась breaking change

// Быстрый hotfix — временный слой compatibility
app.get('/api/v1/users/:id', (req, res) => {
  try {
    const user = getUser(req.params.id);
    // Преобразуем v2 формат обратно в v1
    const v1Response = convertV2toV1(user);
    res.json(v1Response);
  } catch (e) {
    logger.error('Compatibility layer error', e);
    res.status(500).json({ error: 'Internal error' });
  }
});

Итог

Главное правило: Никогда не удаляй, всегда добавляй новое. Backward compatibility — это не премиум фича, это базовый уровень профессионализма. Как говорят: "Первый раз ошибёшься один раз, второй раз ошибаться будешь на тысячи клиентов".

Как обработать ошибку обратной несовместимости изменений? | PrepBro