senior
Как обеспечить обратную совместимость REST API?
Обратная совместимость (backward compatibility) — свойство API, при котором обновлённая версия продолжает корректно работать с существующими клиентами без необходимости их изменения.
Неломающие изменения (non-breaking)
- Добавление нового поля в ответ — клиенты игнорируют неизвестные поля (tolerant reader).
- Добавление нового endpoint-а.
- Добавление нового опционального параметра запроса.
- Добавление нового значения enum (в ответе).
- Расширение допустимого диапазона значений.
Ломающие изменения (breaking)
- Удаление или переименование поля из ответа.
- Изменение типа поля (
"age": 30->"age": "30"). - Удаление endpoint-а.
- Добавление обязательного параметра.
- Изменение кода ответа.
Стратегии обеспечения совместимости
| Стратегия | Описание |
|---|---|
| Версионирование (URL/Header) | /api/v1/users и /api/v2/users работают параллельно |
| Deprecation + Sunset (RFC 8594, 8977) | Заголовки уведомляют о плановом удалении |
| Tolerant Reader (Закон Постела) | Клиент игнорирует неизвестные поля, не зависит от порядка |
| API Evolution | Постепенное развитие: добавление новых полей, deprecated для старых |
| Feature Flags | Управление поведением через заголовки |
Tolerant Reader Pattern
«Будь консервативен в том, что отправляешь, и либерален в том, что принимаешь.»
Пример
// Настройка Jackson для игнорирования неизвестных полей
@JsonIgnoreProperties(ignoreUnknown = true)
public record UserDto(Long id, String name, String email) {}
// Или глобально
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Примеры реализации
Заголовки Deprecation и Sunset:
@GetMapping("/api/v1/users")
public ResponseEntity<List<UserV1Dto>> getUsersV1() {
List<UserV1Dto> users = userService.findAllV1();
return ResponseEntity.ok()
.header("Deprecation", "true")
.header("Sunset", "Sat, 01 Nov 2026 00:00:00 GMT")
.header("Link", "</api/v2/users>; rel=\"successor-version\"")
.body(users);
}
Паттерн эволюции API:
// Шаг 1: Добавляем новое поле, старое оставляем
public record UserDto(
Long id,
String name,
String email,
@Deprecated String userName, // старое поле (deprecated)
String displayName // новое поле
) {
@JsonProperty("userName")
public String getUserName() {
return displayName != null ? displayName : name;
}
}
// Шаг 2: Через несколько релизов удаляем старое поле
Автоматическое обнаружение несовместимых изменений:
openapi-diff --fail-on-incompatible \
api-spec-v1.yaml \
api-spec-v2.yaml
Частые ошибки
- Удаление полей без предварительного deprecated-периода.
- Изменение типа поля без версионирования.
- Добавление обязательного параметра в существующий endpoint.
- Отсутствие автоматической проверки совместимости в CI/CD.
- Бесконечная поддержка всех версий — вовремя удаляйте старые версии.
Как используется в 2026
- openapi-diff и Optic интегрируются в CI/CD для автоматической проверки совместимости.
- API lifecycle management — платформы (Backstage, Kong) управляют жизненным циклом API.
- Contract testing (Pact, Spring Cloud Contract) — стандарт для проверки совместимости.
- Тренд на API Evolution вместо строгого версионирования.
На собеседовании: ключевое — разделить изменения на ломающие и неломающие, знать Tolerant Reader и заголовки Deprecation/Sunset. Частая ошибка — не упомянуть автоматическую проверку совместимости в CI/CD.