Перейти к основному контенту
Tech Path Finder
КурсыИнтервьюКод-ревьюБлог
Tech Path Finder

Персонализированный путеводитель в IT. Квизы, мок-интервью, код ревью и аналитика прогресса.

@potapov_me

Платформа

  • Курсы
  • Прогресс
  • Мок-интервью
  • Код ревью
  • Живое ревью с ИИ
  • Тренажёр переговоров
  • Закладки

Контент

  • Блог
  • Главная
  • Обратная связь

Компания

  • О проекте
  • Тарифы
  • Условия использования
  • Конфиденциальность
  • Согласие на обработку данных
  • Cookie
  • Реквизиты

Аккаунт

  • Войти
  • Зарегистрироваться
  • Профиль

© 2026 Tech Path Finder. Все права защищены.

·ИП Потапов К.С.·Политика конфиденциальности·
Сделано с ❤️ в России
  1. Дизайн API
api_design_review

Дизайн API

REST, versioning, backward compatibility, pagination

Дизайн API в Code Review

Внутренний код можно переписать в любой момент. Опубликованный контракт — нет: у него есть потребители, о существовании половины которых вы не знаете.

#Результат урока

Вы научитесь определять, что в диффе является ломающим изменением, даже когда сигнатура формально не изменилась; проверять пагинацию на устойчивость к вставкам; читать формат ошибок с точки зрения клиента; и задавать про новый эндпоинт три вопроса, после которых половина проблем всплывает сама.

#1. Ключевой вопрос: кто потребители и как они узнают

Прежде чем оценивать форму контракта, надо понять его цену. Она полностью определяется тем, кто им пользуется.

Кто потребительЦена изменения
Один фронтенд, выкатывается вместе с бэкендомПочти ноль: меняйте что угодно
Мобильное приложениеВысокая: старая версия живёт в проде месяцами
Другая команда внутри компанииСредняя: нужен согласованный переход
Внешние интеграторыМаксимальная: изменение — публичное событие

Из этого следует практический вывод: одно и то же изменение может быть nit и blocker в зависимости от ответа. Поэтому первый комментарий к PR, меняющему API, — вопрос: кто это вызывает.

Второй вопрос — как потребитель узнает об изменении. Если ответ «мы им скажем в чате», значит, механизма нет, и однажды не скажут.

Проверьте себя. Для главного эндпоинта вашего проекта перечислите всех известных потребителей. Уверены, что список полный?

Частая ошибка. Считать API внутренним, потому что он не задокументирован публично. Внутренний он или нет, определяют потребители, а не документация.

#2. Ломающие изменения, которые не выглядят ломающими

Явные случаи — удаление поля, переименование, новый обязательный параметр — знают все. Опаснее те, что не меняют схему.

Сужение допустимых значений. Поле принимало любую строку, стало принимать значения из перечисления. Схема не изменилась, старые клиенты сломались.

Изменение семантики при том же имени. Поле total включало доставку, стало не включать. Типы совпадают, тесты зелёные, у клиента неверные суммы — и он об этом даже не узнает.

Изменение кода ответа. Возвращали 200 с {"error": ...}, стали возвращать 422. Клиент, проверяющий response.ok, начнёт падать.

Ужесточение валидации. Раньше лишние поля в теле игнорировались, теперь отвергаются. Клиент, отправлявший отладочное поле, сломан.

Изменение порядка в списке без явной сортировки. Клиент полагался на порядок, потому что он был стабилен, — а порядок никто не обещал. Формально виноват клиент, практически — сломались вы.

Изменение точности или формата. Дата была 2026-07-28, стала 2026-07-28T00:00:00Z. Цена была числом, стала строкой ради точности — правильное решение, ломающее всех.

class OrderResponse(BaseModel): - total: float + total: Decimal # сериализуется как строка "1234.56"

Две строки в диффе, полностью сломанный клиент, который делает total * quantity.

Проверьте себя. Найдите в истории проекта изменение, которое сломало клиента, хотя схема не менялась.

Частая ошибка. Проверять совместимость сравнением схем. Инструмент увидит удаление поля и не увидит изменение смысла.

#3. Совместимое расширение

Работающая стратегия — добавлять, а не менять. Новое опциональное поле в ответе безопасно почти всегда; новое опциональное поле в запросе — тоже.

Про «никогда не удаляйте поля» надо уточнить: это не значит «копите вечно». Схема с дублирующими парами name/full_name, age/years быстро становится нечитаемой, и новые клиенты не понимают, что использовать.

Работающий порядок — с фиксированным сроком:

  1. Добавить новое поле, старое оставить, оба заполняются.
  2. Пометить старое устаревшим: в документации, в заголовке Deprecation, в логах — с указанием даты снятия.
  3. Измерить, кто ещё его читает. Без телеметрии по полям этот шаг невозможен, и именно поэтому поля не удаляют никогда.
  4. Удалить после даты, если потребителей не осталось.

Ревьюеру важен третий пункт: если в PR добавляется поле-замена, разумный вопрос — «как мы узнаем, что старое можно убрать?».

Версионирование целого API — тяжёлый инструмент. Он оправдан при внешних интеграторах и почти всегда избыточен для внутреннего фронтенда: две версии означают двойную поддержку, а не свободу.

Проверьте себя. Есть ли в проекте поле, помеченное deprecated больше года назад? Почему оно всё ещё здесь?

Частая ошибка. Заводить /v2 из-за одного изменённого поля. Дешевле добавить поле, чем поддерживать две версии всего API.

#4. Пагинация

Пагинация со смещением проста и имеет два свойства, которые надо проверять в ревью.

GET /orders?limit=20&offset=40

Первое: на больших смещениях база всё равно читает и отбрасывает все предыдущие строки — offset=100000 медленный независимо от индексов. Второе, менее известное: при вставках и удалениях между запросами страницы съезжают, и клиент видит одну запись дважды или пропускает её.

GET /orders?limit=20&cursor=eyJpZCI6MTIzfQ

Курсорная пагинация от этого свободна: курсор кодирует позицию в упорядоченном наборе, и вставка новых записей её не сдвигает. Цена — нельзя перейти на произвольную страницу.

Практический критерий для ревью: если данные упорядочены по времени и активно дописываются (лента, журнал, история), нужен курсор. Если это административная таблица с номерами страниц и объём заведомо небольшой — смещение допустимо.

Обязательные вопросы к любому списочному эндпоинту:

  • Есть ли предел по умолчанию? Эндпоинт без limit однажды отдаст всю таблицу.
  • Есть ли максимум? limit=1000000 не должен приниматься.
  • Задан ли порядок явно? Без ORDER BY пагинация не имеет смысла: без гарантированного порядка страницы не согласованы между собой.
  • Уникален ли ключ сортировки? Сортировка по created_at при одинаковых значениях даёт нестабильный порядок — нужен тай-брейк по id.

Проверьте себя. Запросите у своего API страницу со смещением 100 000 и посмотрите на время ответа.

Частая ошибка. Курсор, содержащий смещение. Это пагинация со смещением с лишним шагом кодирования и всеми её недостатками.

#5. Ошибки

Ответ об ошибке — тоже контракт, и его форма определяет, сможет ли клиент реагировать осмысленно.

// ❌ клиент может только показать текст пользователю {"error": "Something went wrong"} // ❌ хуже: 200 с ошибкой внутри — клиент проверяет response.ok и не замечает {"status": "error", "message": "Insufficient funds"}
// ✅ машиночитаемый код, человекочитаемое сообщение, детали по полям { "error": { "code": "insufficient_funds", "message": "На счёте недостаточно средств", "details": {"required": "1500.00", "available": "820.50"}, "request_id": "01J2K..." } }

Ключевое — стабильный code, по которому клиент ветвится. Текст сообщения меняется и переводится, код — нет. Если клиенту приходится разбирать message строкой, чтобы понять, что случилось, контракт спроектирован неверно.

request_id в теле ошибки экономит часы поддержки: пользователь присылает его вместе с жалобой, и запрос находится в логах мгновенно.

Что ещё проверять: коды состояния соответствуют смыслу (404 для отсутствия, 403 для запрета, 409 для конфликта, 422 для невалидных данных); ошибка не раскрывает внутренности (текст SQL-исключения, трассировка стека); частичный успех обрабатывается явно, а не молча.

Проверьте себя. Может ли ваш клиент отличить «недостаточно средств» от «карта заблокирована», не разбирая текст сообщения?

Частая ошибка. Один код 400 на все ошибки валидации без указания поля. Клиент не может подсветить проблемное поле в форме.

#6. Идемпотентность и повторы

Любой клиент повторяет запрос по таймауту — это встроено в HTTP-клиенты, мобильные библиотеки и балансировщики. Значит, каждый небезопасный метод должен либо выдерживать повтор, либо иметь ключ идемпотентности.

POST /payments Idempotency-Key: 8f1c...

Правило: повтор с тем же ключом возвращает тот же результат, а не создаёт второй объект и не отдаёт ошибку. Возврат 409 Conflict на повтор — распространённая, но неудачная реализация: клиент не может отличить «мой повтор прошёл» от «кто-то другой занял ключ».

Для GET, PUT и DELETE идемпотентность обеспечивается семантикой. Опасен именно POST, и особенно всё, что связано с деньгами и отправкой сообщений.

Проверьте себя. Найдите в API метод POST, создающий что-то важное. Что произойдёт при двойной отправке?

Частая ошибка. Полагаться на кнопку, заблокированную на фронтенде. Повтор приходит не от кнопки, а от сетевого слоя.

#7. Разбор: PR «Переход на новый формат профиля»

Мобильное приложение в проде, релизный цикл в магазинах — до трёх недель. Дифф на 40 строк.

class ProfileResponse(BaseModel): - name: str - balance: float - created: str # "2026-07-28" + full_name: str + balance: Decimal + created_at: datetime + tier: str # "basic" | "gold" | "platinum" @app.get("/api/profile") def get_profile(user=Depends(current_user)): - return ProfileResponse( - name=user.name, balance=user.balance, created=str(user.created.date()) - ) + return ProfileResponse( + full_name=user.full_name, + balance=user.balance, + created_at=user.created, + tier=compute_tier(user), + ) @app.get("/api/orders") -def list_orders(user=Depends(current_user), limit: int = 20, offset: int = 0): - return Order.objects.filter(user=user)[offset:offset + limit] +def list_orders(user=Depends(current_user), limit: int = 20, cursor: str | None = None): + qs = Order.objects.filter(user=user).order_by("-created_at") + if cursor: + qs = qs.filter(created_at__lt=decode_cursor(cursor)) + return qs[:limit]

Что видит ревьюер. Каждое изменение по отдельности — улучшение. Decimal вместо float для денег правильно, datetime информативнее строки, курсорная пагинация лучше смещения. И при этом весь дифф — набор ломающих изменений, выпускаемых одновременно, при клиенте, который обновится через три недели.

name → full_name: экран профиля покажет пустое имя. balance: float → Decimal сериализуется в строку — арифметика на клиенте сломается. created → created_at с другим форматом — разбор даты упадёт. Смена offset на cursor: старое приложение отправит offset, он будет проигнорирован, и пользователь увидит первую страницу вместо второй — то есть бесконечную прокрутку, зацикленную на первых двадцати заказах.

Отдельно: новое поле tier со значениями-строками. Что произойдёт, когда добавится четвёртый уровень? Старый клиент со switch по трём значениям попадёт в непредусмотренную ветку.

И тай-брейк в курсорной пагинации: created_at не уникален, у заказов, созданных в одну секунду, порядок не определён — часть записей может пропасть между страницами.

Комментарии в PR:

schemas.py:2 · blocker Четыре ломающих изменения в одном релизе при мобильном клиенте, который обновится через три недели. Каждое по отдельности верное, но вместе они выведут из строя экраны профиля и списка заказов у всех текущих пользователей.

Предлагаю: новые поля добавить рядом со старыми, старые оставить и заполнять, пометить deprecated с датой снятия — например, через два месяца. Удалять после того, как телеметрия покажет, что старые поля больше не читают.

schemas.py:4 · blocker balance: float → Decimal сериализуется как строка "820.50". Клиент делает арифметику с этим значением — сломается молча, без исключения, с неверными суммами на экране. Если переход нужен (а он нужен), добавьте balance_amount: Decimal рядом и оставьте balance до снятия.

orders.py:2 · blocker Замена offset на cursor без переходного периода: старый клиент отправит offset, параметр будет молча проигнорирован, и пользователь получит первую страницу на каждый запрос — бесконечная прокрутка зациклится. Поддержите оба параметра: cursor в приоритете, offset работает как раньше.

orders.py:5 · major Курсор по created_at без тай-брейка: значение не уникально, у заказов из одной секунды порядок не определён, и записи будут пропадать между страницами. Сортируйте по (created_at, id) и кодируйте в курсор оба значения.

schemas.py:6 · major tier как строка из трёх значений — на клиенте это switch, который попадёт в непредусмотренную ветку при добавлении четвёртого уровня. Опишите в документации, что список расширяемый, и попросите клиент иметь ветку по умолчанию. Иначе следующий уровень станет ломающим изменением.

orders.py:7 · major limit без верхней границы: limit=1000000 примется. Ограничьте сотней.

schemas.py:1 · question Есть ли у нас телеметрия по чтению полей? Без неё «удалим, когда никто не читает» превращается в «не удалим никогда». Если нет — стоит завести хотя бы логирование версии клиента.

Чем закончилось. PR разделили на два. В первый вошли добавления: новые поля рядом со старыми, cursor рядом с offset, тай-брейк в сортировке, предел на limit. Во второй, запланированный через два месяца, — удаление старых полей.

Заголовок Deprecation с датой добавили в ответы, а логирование версии клиента по полям завели отдельным тикетом — без него срок снятия было бы невозможно обосновать.

Через два месяца выяснилось, что created всё ещё читает внутренний отчётный сервис, о котором никто не помнил. Именно для таких случаев нужен третий шаг с телеметрией.

#8. Чек-лист

ПОТРЕБИТЕЛИ перечислены; известно, как они узнают об изменении СЛОМ проверены не только схема, но и семантика, коды, форматы, валидация ПЕРЕХОД новое добавлено рядом со старым; у устаревшего есть дата снятия ТЕЛЕМЕТРИЯ известно, кто читает поле, которое планируется удалить СПИСКИ предел по умолчанию и максимум; порядок задан явно и уникален КУРСОР для дописываемых наборов; с тай-брейком по уникальному ключу ОШИБКИ стабильный код, детали по полям, request_id, честный HTTP-статус ИДЕМПОТЕНТНОСТЬ у POST с побочными эффектами есть ключ; повтор возвращает тот же результат РАСШИРЯЕМОСТЬ перечисления описаны как расширяемые

#Что дальше

Авторизация на уровне эндпоинтов — Безопасность: глубокий разбор. Пределы, повторы и деградация под нагрузкой — Масштабируемость. Документирование контракта и changelog — Рецензирование документации.


Ключевая мысль: ломающим изменение делает не дифф, а срок жизни старого клиента. Один и тот же PR безопасен при синхронной выкатке и катастрофичен при мобильном приложении в проде.

Далее: Глубокая проверка безопасности