awesome-everything EN
↑ Обратно к восхождению

API

Моделирование REST-ресурсов: существительные, нужные клиенту, а не твои глаголы и таблицы

Суть REST API — это модель ресурсов, важных клиенту. Два провала, которые бьют в проде: превращать действия в endpoint-глаголы и зеркалить таблицы БД, из-за чего любое изменение схемы ломает каждого клиента.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на junior-высоте — поверхность
◷ 16 min

Миграция переименовывает users.full_name в users.display_name. Рутина. Деплой уходит в пятницу днём, и три версии мобильного приложения «в полях» начинают рисовать пустые поля имени, потому что API сериализовал строки БД прямо в JSON, а клиенты завязались на full_name. Чинить нечего — колонка и есть контракт. Откатывай миграцию, выкатывай хотфикс, который никто не сможет установить две недели, и добавляй «переименовать колонку» в список того, что теперь требует скоординированного релиза.

Ресурсы — существительные; действия — не endpoint’ы

Главный ход REST — смоделировать домен как ресурсы, адресуемые существительные: заказ, пользователь, инвойс, — и дать небольшому фиксированному набору HTTP-глаголов действовать на них. GET /orders/42 читает, PATCH /orders/42 обновляет, DELETE /orders/42 удаляет. Глагол живёт в методе, а не в URL. В тот миг, когда ты пишешь POST /createOrder или GET /getUserById, ты перестал делать REST и начал делать RPC с лишними шагами: URL теперь несёт глагол, который HTTP-метод уже выражает, и ты потерял кэшируемость, единообразный интерфейс и каждый инструмент, рассуждающий о ресурсах.

Две конвенции, которые сеньор считает не обсуждаемыми ради консистентности, потому что клиенты их распознают по паттерну:

  • Множественные существительные коллекций: /orders, /orders/42. Коллекция — это /orders; элемент — /orders/{id}. Не мешай /order и /orders.
  • Стабильные непрозрачные ID, а не позиционные или угадываемые. ID в /orders/42 — это идентичность ресурса; клиенты сохраняют его и перезапрашивают, поэтому он не должен меняться при пересортировке или re-shard.

Фильтрация, сортировка и пагинация — не новые endpoint’ы, а query-параметры на коллекции: GET /orders?status=open&sort=-created_at&page=2. Изобретать /openOrders и /recentOrders как отдельные пути — та же RPC-ошибка на уровне коллекции.

Проблема не-CRUD действий

В реальных доменах есть действия, которые не create/read/update/delete: отменить заказ, опубликовать пост, вернуть платёж, перезапустить джобу. Наивный фикс — POST /orders/42/cancel читается как endpoint-глагол, и пуристы кривятся. Но это то самое место, где побеждает прагматизм, и даже гайдлайны Microsoft и Thoughtworks это принимают: переход состояния, который не является естественным обновлением поля, лучше всего моделировать как action-подресурс или transition-ресурс.

Два чистых паттерна:

  1. Action-подресурс: POST /orders/42/cancel. Да, это глагол в URL, но он привязан к ресурсу и использует POST (неидемпотентный, с побочным эффектом) — честно. Большинство крупных публичных API (Stripe, GitHub) делают ровно так.
  2. Transition-ресурс (трюк «онаунивания»): смоделируй само действие как ресурс. Возврат — не глагол на платеже, а Refund, который ты создаёшь: POST /payments/42/refunds. Теперь у возврата есть свой ID, статус и аудит-трейл, и ты можешь сделать GET /payments/42/refunds, чтобы получить их список. Это строго лучше, когда у действия есть собственный жизненный цикл или нужна запись о нём.

Анти-паттерн, которого надо избегать: подделывать переход через PATCH /orders/42 {"status": "cancelled"}. Выглядит RESTful, но это ловушка — ты выставил внутреннее поле состояния как доступное клиенту на запись, поэтому клиент может выставить status во что угодно, обойти бизнес-правила, гейтящие настоящую отмену, и загнать заказ в состояние, которого твой код никогда не ожидал.

Ты хочешь…Не надоНадо
Создать заказPOST /createOrderPOST /orders
Получить открытые заказы, новые первымиGET /openOrdersGET /orders?status=open&sort=-created_at
Отменить заказPATCH /orders/42 {"status":"cancelled"}POST /orders/42/cancel
Вернуть платёж (с аудитом)POST /refundPayment?id=42POST /payments/42/refunds

Гранулярность: чрезмерно вложенный, болтливый API

Вложенность выражает владение: /orders/42/items — это элементы, принадлежащие заказу 42. Читается хорошо — пока не продолжишь дальше. /customers/7/projects/3/orders/42/items/9/discounts — это реальная форма, которую команды отгружают, и это катастрофа в замедленной съёмке. URL хрупкие (перемести элемент между заказами — и каждый закодированный путь ломается), и хуже того, глубокая вложенность вынуждает болтливого клиента: чтобы отрисовать один экран, приложение должно пройти иерархию — получить клиента, затем его проекты, затем заказы каждого проекта, затем элементы каждого заказа — это N+1 проблема, поднятая до сетевых round-trip’ов. Дашборд, которому нужно 8 сущностей, превращается в 8+ последовательных HTTPS-вызовов, каждый платит свой налог на latency; при 80 мс на round-trip этот экран занимает 640 мс до любого рендеринга.

Правило большого пальца сеньора: вкладывай максимум на один уровень, два — как абсолютный потолок. Дальше адресуй ресурсы по их собственному ID в корне и используй query-параметры для связи. Вместо /customers/7/projects/3/orders выстави /orders?customer=7&project=3. У заказа есть собственная стабильная идентичность; добираться до него не должно требовать всей его родословной. Для проблемы болтливого экрана конкретно ответ — более грубое представление: позволь клиенту запросить связанные данные через ?expand=items,customer или ?include=..., чтобы один вызов вернул то, что нужно экрану, вместо принудительных N проходов.

Почему это работает

Почему POST /orders/42/cancel терпят, когда «глаголы в URL» — кардинальный грех? Потому что альтернатива — поле status, доступное на запись, — хуже. Endpoint отмены — это защищённый переход: сервер прогоняет правила (заказ уже отгружен? уже возвращён?) и либо выполняет весь переход, либо отклоняет его. Поле на запись отдаёт клиенту ключи от машины состояний. Чистота «без глаголов в URL» — меньшая цена, чем незащищённое поле состояния.

Representation vs ресурс и ловушка дырявой абстракции

Ресурс — это концептуальная вещь (заказ). Representation (представление) — одна сериализация ресурса (JSON, который ты возвращаешь; XML-вариант; краткий вид vs полный). У одного ресурса может быть много представлений; ресурс стабилен, представление — это выбор. Именно это различие даёт твоему API пережить изменения — а игнорирование его и вызвало пятничную катастрофу из Hook.

Когда API сериализует строки БД напрямую, representation и есть таблица. Теперь схема таблицы — публичный контракт: переименуй колонку — сломаешь каждого клиента; добавь колонку NOT NULL — старые записи падают; выстави внутренний флаг is_deleted — и клиент начнёт от него зависеть. Фикс — явный mapping-слой (DTO, сериализатор, view model), который переводит между формой хранения и формой на проводе. Он стоит пары минут на endpoint и покупает свободу рефакторить базу без скоординированного релиза клиентов. Это самое высокорычажное решение в дизайне API: representation — это контракт, которым владеешь ты, а не случайность твоего ORM.

HATEOAS — теоретический эндшпиль здесь: ответы несут гипермедиа-ссылки (_links: { cancel: "/orders/42/cancel" }), чтобы клиенты открывали доступные действия вместо хардкода URL, а сервер мог свободно эволюционировать URL. В теории это серебряная пуля развязки. На практике это самое недоиспользуемое ограничение REST — большинство «REST» API находятся на уровне 2 по Ричардсону (ресурсы + глаголы, без гипермедиа), потому что клиентские фреймворки редко потребляют ссылки, а дисциплина редко окупается вне крупных гипермедиа-нативных экосистем. Знай, что это, знай, почему это редкость, и не давай пуристу блокировать твой релиз из-за её отсутствия.

Выбери лучший вариант

Экран показывает клиента с его проектами и заказами каждого проекта. Текущий API — /customers/{id}/projects/{id}/orders, и рендеринг занимает 9 последовательных вызовов. Выбери редизайн.

Викторина

Нужно дать клиентам возвращать платёж, а финансы хотят, чтобы каждый возврат был аудируемым со своим статусом. Какая модель самая чистая?

Викторина

Почему сериализация строк БД прямо в JSON в ответах кусает тебя потом?

Расставь шаги по порядку

Расставь шаги, чтобы смоделировать возможность «опубликовать черновик поста» по-RESTful:

  1. 1 Определи ресурс: пост — это существительное → /posts/{id}
  2. 2 Распознай, что 'publish' — переход состояния, а не CRUD-поле, которое клиент должен писать напрямую
  3. 3 Смоделируй это как защищённый action-подресурс: POST /posts/{id}/publish
  4. 4 Сервер прогоняет правила (уже опубликован? автор имеет право?) и выполняет весь переход
  5. 5 Верни новое представление поста (status: published) — через сериализатор, а не сырые строки БД
Вспомните перед уходом
  1. 01
    Коллега хочет добавить «отменить заказ» как PATCH /orders/{id} с {"status": "cancelled"}. Объясни, почему это ловушка и что делать вместо этого.
  2. 02
    Почему «не сериализуй строки БД напрямую» считается самым высокорычажным решением в дизайне API и что реально покупает mapping-слой?
Итог

REST API — это модель ресурсов, существительных, важных клиенту, адресуемых стабильными ID, где глагол живёт в HTTP-методе, а не в URL. Используй множественные существительные коллекций и клади фильтрацию, сортировку и пагинацию в query-параметры, а не плоди новые endpoint’ы. Не-CRUD действия — настоящий тест: моделируй переход состояния как защищённый action-подресурс (POST /orders/{id}/cancel) или, когда у него свой жизненный цикл, как transition-ресурс (POST /payments/{id}/refunds) — но никогда как поле status на запись клиенту, которое разблокирует машину состояний. Держи вложенность мелкой (один уровень, два максимум) и тянись к query-параметрам плюс расширению полей, чтобы один экран не разворачивался в N последовательных round-trip’ов. И главное — сериализуй через mapping-слой, чтобы representation было контрактом, которым владеешь ты, а не зеркалом таблиц БД: это одно решение и держит базу рефакторимой без поломки каждого клиента. HATEOAS — изящная теория самоописывающих ссылок; знай её и знай, почему уровень 2 REST — это то, что почти все реально отгружают.

Продолжить восхождение ↑REST-моделирование: тест с выбором ответа
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.