Дизайн API в Java
У опубликованного API есть свойство, которое отличает его от обычного кода: его нельзя тихо переделать. Внутренний класс переименовывают за минуту, а поле в ответе, на которое завязались чужие клиенты, живёт годами после того, как перестало быть нужным.
Отсюда и вопросы: как назвать ресурсы, как отдавать списки, что считается ломающим изменением и как выпускать новое, не разрушив старое.
// Формулировки: «как спроектируешь ресурсы?», «как сделаешь постраничную выдачу?», «что такое ломающее изменение?», «как версионируешь API?».
Ресурсы и версии
Основа стиля REST простая: ресурсы - существительные, действия над ними - методы HTTP. GET /orders/42/items вместо POST /getOrderItems. Вложенность показывает принадлежность, но глубже двух уровней читается плохо: /orders/42/items/7/comments/3 уже никто не запомнит.
Действия, которые не ложатся на четыре метода, оформляют как ресурс-событие. Отмену заказа делают через POST /orders/42/cancellation, а не через POST /cancelOrder. Так у действия появляется своё состояние, которое можно потом запросить.
// Версию чаще всего кладут в путь: /v1/orders. Это не самый академичный способ, зато самый читаемый в логах и самый простой для маршрутизации. Главное правило версионирования - не плодить версии по любому поводу: каждая живая версия это отдельный код, который надо поддерживать и тестировать.
- ресурс
- существительное в адресе; действие выражается методом HTTP
Списки: постраничная выдача и фильтры
Коллекция без ограничения размера - мина. Сегодня в таблице сто строк, через год миллион, и запрос GET /orders кладёт и базу, и приложение. Поэтому на списках всегда есть предел по умолчанию и максимум, который клиент не может превысить.
Способов нарезки два. По смещению (offset и limit) - привычный и удобный для страниц с номерами, но у него есть дефект: пока пользователь листает, в начало списка приходят новые записи, всё съезжает, и часть строк он видит дважды, а часть не видит вовсе. По курсору - клиент получает метку последней увиденной записи и просит «следующие после неё»; вставки в начало ничего не сдвигают. Для лент и выгрузок берут курсор, для админок со страницами - смещение.
// Сортировка обязана быть однозначной. Если сортировать только по дате, а дат-дубликатов много, порядок между ними не определён, и страницы поедут даже без вставок. Лечится добавлением идентификатора вторым ключом сортировки.
- выдача по смещению
- offset и limit: просто, но страницы едут при вставках
- выдача по курсору
- метка последней записи: устойчива к вставкам в начало
Совместимость как дисциплина
Ломающее изменение - любое, после которого работающий клиент перестаёт работать. Удалить поле, переименовать его, сузить набор допустимых значений, сделать необязательное поле обязательным, поменять тип - всё это ломает. А вот ДОБАВИТЬ необязательное поле безопасно, если клиенты умеют игнорировать незнакомое.
Отсюда договорённость, на которой всё держится: клиент обязан не падать от полей, которых не знает. Её проговаривают в документации явно, потому что молчаливое предположение тут не работает - находится клиент, который парсит ответ строго и валится от новой строки.
// Удаление поля делают в три шага, а не разом. Сначала объявляют его устаревшим в документации и начинают отдавать заменяющее. Потом смотрят по метрикам, кто ещё читает старое. И только когда таких клиентов не осталось, удаляют. Без второго шага получается «мы предупреждали в рассылке» - и упавший клиент на проде.
- ломающее изменение
- после него работающий клиент перестаёт работать
Как отвечать: «Что такое ломающее изменение и как его избежать?»
Ломающее - это изменение, после которого уже работающий клиент перестаёт работать: удалить поле, переименовать, поменять тип, сделать необязательное обязательным, сузить набор допустимых значений. Безопасно только добавление необязательного поля, и то при условии, что клиенты игнорируют незнакомые поля - эту договорённость я прописываю в документации явно, а не подразумеваю. Когда поле всё-таки надо убрать, я делаю это в три шага: помечаю устаревшим и одновременно отдаю замену, потом по метрикам смотрю, кто ещё читает старое поле, и удаляю, только когда таких клиентов не осталось. Версию в пути завожу для по-настоящему крупных развилок, а не под каждое изменение: каждая живая версия это отдельный код, который надо поддерживать и тестировать.
Почему это сильный ответ: дано определение через последствие для клиента, перечислены конкретные виды изменений и описан рабочий процесс удаления с проверкой по метрикам, а не по рассылке.
На чём валят
- −Глаголы в адресах: POST /getOrderItems. Действие выражается методом, а адрес называет ресурс.
- −Коллекция без предела размера. Сегодня сто строк, через год миллион и упавшая база.
- −Выдача по смещению для ленты с постоянными вставками. Страницы едут, часть записей теряется, часть дублируется.
- −Сортировка по неуникальному полю. Порядок внутри одинаковых значений не определён, страницы поедут сами по себе.
- −Удалить поле, предупредив в рассылке. Проверять надо по метрикам обращений, а не по факту рассылки.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Как в REST API отдавать большие коллекции?A)Одним ответом целиком, каким бы большим он ни был — клиент сам разберётся с объёмом данныхB)Через отдельный запрос на каждый элемент коллекции по его идентификатору, без общего спискаC)Постранично: параметры page/size (или курсор) и метаданные о всего/страницахD)Сжимая весь результат в один архив и отдавая ссылку на скачивание файла вместо JSON-ответа
показать ответ и разбор
+C)Постранично: параметры page/size (или курсор) и метаданные о всего/страницах// разбор: Большие коллекции отдают ПОСТРАНИЧНО: клиент запрашивает страницу (?page=2&size=20 или cursor-based ?after=...&limit=20), сервер возвращает срез плюс метаданные (общее число, есть ли следующая). Это защищает сервер (память, время запроса) и клиента (объём) и делает ответы предсказуемыми. Offset-пагинация проста, но дорога на глубоких страницах и «плывёт» при вставках; cursor/keyset-пагинация стабильнее и быстрее на больших данных. Вместе с пагинацией дают сортировку и фильтры в query. Отдавать неограниченную коллекцию целиком — частая ошибка масштаба.
- Как оформлять ошибки в REST API единообразно?A)Возвращать 200 OK, а признак ошибки и её текст класть в отдельное поле JSON-ответаB)Отдавать HTML-страницу с описанием ошибки, как это делает сервер для обычного браузера по умолчаниюC)Передавать код и текст ошибки в URL следующего запроса, чтобы клиент увидел их при редиректеD)Корректный HTTP-код + структурированное тело (например, ProblemDetail/RFC 7807)
показать ответ и разбор
+D)Корректный HTTP-код + структурированное тело (например, ProblemDetail/RFC 7807)// разбор: Хорошее API отдаёт КОРРЕКТНЫЙ статус-код (клиент и мониторинг по нему понимают исход) И СТРУКТУРИРОВАННОЕ тело ошибки единого формата: тип/код ошибки, человекочитаемое сообщение, детали (какие поля невалидны), traceId для диагностики. Стандарт для тела — RFC 7807 Problem Details (в Spring Boot 3 — класс ProblemDetail). Централизуют это через @RestControllerAdvice. Антипаттерны: 200 с error в теле (ломает семантику/ретраи/алерты), HTML-страница вместо JSON, разный формат ошибок в разных эндпоинтах. Единый контракт ошибок так же важен, как контракт успешных ответов.
- Где выполнять валидацию входных данных API?A)На сервере обязательно (клиентская — лишь для UX), до бизнес-логикиB)Только на клиенте: раз браузер уже проверил форму, серверу повторять эти проверки не требуетсяC)Только в базе данных через ограничения — приложению проверять входные данные не нужноD)Валидацию можно не делать, если API закрыт токеном: аутентифицированные клиенты присылают лишь корректное
показать ответ и разбор
+A)На сервере обязательно (клиентская — лишь для UX), до бизнес-логики// разбор: Валидацию на СЕРВЕРЕ делать ОБЯЗАТЕЛЬНО: клиентскую (в браузере) легко обойти прямым запросом (curl/Postman/злоумышленник), поэтому она — только про UX (быстрая обратная связь), а не про безопасность/корректность. Сервер — источник истины: проверяет данные на ГРАНИЦЕ (в контроллере через @Valid + Bean Validation), ДО бизнес-логики, и при нарушении отвечает 400/422 с деталями. Ограничения БД — последний рубеж (целостность), но опираться только на них плохо: поздно, скупые сообщения, не выражают бизнес-правила. Валидация: клиент — UX, сервер — истина, БД — целостность.
- Как менять API, не ломая существующих клиентов (обратная совместимость)?A)Свободно переименовывать и удалять поля — клиенты обязаны сами следить за изменениями и подстраиватьсяB)Делать аддитивные изменения: добавлять поля/эндпоинты, не удаляя и не переименовывая старыеC)Менять типы существующих полей на лету, ведь JSON слабо типизирован и клиенты это стерпятD)Выпускать новую версию на каждое, даже самое мелкое, изменение, включая добавление одного поля
показать ответ и разбор
+B)Делать аддитивные изменения: добавлять поля/эндпоинты, не удаляя и не переименовывая старые// разбор: Обратно совместимы АДДИТИВНЫЕ изменения: добавить НОВОЕ ОПЦИОНАЛЬНОЕ поле в ответ (старые клиенты его игнорируют), новый эндпоинт, новый необязательный параметр запроса. ЛОМАЮЩИЕ — удалить/переименовать поле, сменить его тип/семантику, сделать необязательное обязательным, изменить коды/формат ошибок: их вводят через НОВУЮ ВЕРСИЮ (/v2) с периодом сосуществования и деприкейта старой. Клиенты пишут толерантно (игнорируют неизвестные поля — принцип Postel). Так API эволюционирует годами без «большого взрыва» на каждый релиз.
- Что даёт OpenAPI/Swagger-спецификация для API?A)Автоматически ускоряет API, кэшируя описанные в спецификации эндпоинты на стороне клиентаB)Заменяет собой саму реализацию: по спецификации сервис работает без написания кода контроллеровC)Машиночитаемый контракт: документация, генерация клиентов/серверов, контрактные тестыD)Шифрует запросы и ответы согласно описанной схеме, обеспечивая безопасность передачи данных
показать ответ и разбор
+C)Машиночитаемый контракт: документация, генерация клиентов/серверов, контрактные тесты// разбор: OpenAPI (спецификация; Swagger — инструменты вокруг неё) — МАШИНОЧИТАЕМОЕ описание API: эндпоинты, методы, параметры, схемы запросов/ответов, коды ошибок. Из него получают: интерактивную документацию (Swagger UI), генерацию клиентских SDK и серверных заглушек, контрактные тесты, валидацию запросов/ответов, mock-серверы. Спецификацию либо пишут первой (design-first — контракт до кода), либо генерируют из аннотаций кода (code-first, springdoc-openapi). Это единый источник правды о контракте, синхронизирующий фронт, бэк и потребителей.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.