Webrium
Подписаться

REST API: что решить до первого маршрута

Как правильно структурировать данные в API, чтобы избежать дублирования и упростить поддержку

Разделение подходов: проектирование от экранов и от структуры данныхЭкраны интерфейсаСтруктура данных

Начинают не с того: почему первый маршрут - это ловушка

Первое, что делают при проектировании REST API - рисуют первый маршрут. Обычно это что-то вроде /users или /tasks. Кажется логичным: нужно же с чего-то начать, а маршруты - это то, что видит клиент. Но этот подход ведёт в тупик.

Когда начинают с маршрутов, неизбежно возникает проблема дублирования логики. Например, задача может принадлежать и проекту, и пользователю. Тогда появляются маршруты /projects/{id}/tasks и /users/{id}/tasks. Клиенту приходится выбирать, какой из них использовать, а серверу - поддерживать оба. Это усложняет код и увеличивает вероятность ошибок.

Проблема в том, что маршруты - это лишь способ доступа к данным, а не сами данные. Если сначала определить сущности и их отношения, маршруты получатся сами собой. Например, если задача принадлежит проекту, то маршрут /projects/{id}/tasks будет единственным правильным решением.

Тупик: проектирование от экранов интерфейса

Самый распространённый (и ошибочный) подход - проектировать API от экранов интерфейса. На экране список задач - значит нужен /tasks. На экране создания задачи - значит нужен POST /tasks. Кажется, что это правильно: API должно обслуживать интерфейс.

Но этот подход не работает по нескольким причинам:

  1. Интерфейс меняется чаще, чем данные. Если API жёстко привязан к экранам, каждое изменение интерфейса потребует изменений в API. Например, если на новом экране нужны задачи с фильтрами по статусу и исполнителю, придётся добавлять параметры ?status=open&assignee=123. А что, если позже понадобится другой фильтр? API начнёт разрастаться.

  2. Один и тот же экран может использовать разные данные. Например, экран задач может показывать задачи проекта, задачи пользователя или все задачи системы. Если API привязан к экрану, придётся создавать отдельные маршруты для каждого случая.

  3. Разные клиенты могут использовать одни и те же данные по-разному. Мобильное приложение, веб-интерфейс и сторонний сервис могут по-разному отображать одни и те же данные. Если API привязан к одному клиенту, другим будет неудобно.

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

Коды ответа: почему “200 с ошибкой” - это минное поле

Коды HTTP-ответов - это не просто числа. Они управляют поведением всей инфраструктуры между клиентом и сервером: прокси, кэши, балансировщики, библиотеки клиентов.

Когда сервер возвращает 200 OK с телом { "error": "Not found" }, происходит следующее:

  1. Прокси видит код 200 и считает ответ успешным
  2. Прокси кэширует ответ
  3. Клиент получает кэшированный ответ с ошибкой
  4. Клиент не понимает, что ресурс не существует

Правильные коды ответов:

  • 200 OK - успешный запрос с телом
  • 201 Created - успешное создание ресурса (с телом нового ресурса)
  • 204 No Content - успешный запрос без тела (например, удаление)
  • 400 Bad Request - клиентская ошибка (невалидные данные)
  • 401 Unauthorized - неавторизованный запрос
  • 403 Forbidden - доступ запрещён
  • 404 Not Found - ресурс не найден
  • 409 Conflict - конфликт состояний (например, дубликат)
  • 500 Internal Server Error - серверная ошибка

Код 200 с полем ошибки - это антипаттерн, который ломает всю цепочку обработки запросов. Если запрос не может быть выполнен, нужно возвращать соответствующий код 4xx или 5xx.

Форма ошибки: почему единообразие критично

Ошибки в API должны быть предсказуемыми. Клиент должен уметь обработать любую ошибку, не зная заранее, какая именно произойдёт.

Пример правильной структуры ошибки:

{
  "error": {
    "code": "not_found",
    "message": "Task with ID 123 not found",
    "details": {
      "id": 123
    }
  }
}

Здесь:

  • code - машиночитаемый идентификатор ошибки. Он не меняется при изменении текста сообщения и позволяет клиенту программно обрабатывать ошибки.
  • message - текст для человека. Он может меняться, но должен быть понятным.
  • details - дополнительные данные об ошибке (например, ID несуществующего ресурса).

Если клиент видит code: "not_found", он знает, что ресурс не существует, и может предпринять соответствующие действия. Если же клиент видит только текстовое сообщение, ему придётся парсить его, что ненадёжно и ломко.

Изменения без поломок: правила эволюции API

API неизбежно меняется. Но изменения не должны ломать существующих клиентов.

Что можно делать:

  • Добавлять новые поля в ответы. Клиенты должны игнорировать неизвестные поля.
  • Добавлять новые маршруты. Существующие клиенты их просто не будут использовать.

Что нельзя делать:

  • Переименовывать существующие поля. Это сломает клиентов, которые используют старые имена.
  • Удалять поля без предупреждения. Если поле устарело, его нужно пометить как deprecated и удалить в следующей мажорной версии.
  • Изменять тип существующих полей. Это сломает клиентов, которые ожидают определённый тип данных.

Где должна жить версия API:

  1. В пути: /v1/tasks. Просто, но усложняет миграцию клиентов.
  2. В заголовке: Accept: application/vnd.company.v1+json. Гибко, но сложнее для тестирования.

Лучше избегать версионирования как можно дольше. Если API спроектирован правильно, большинство изменений можно внести без поломок.

Списки: как не утонуть в данных

Списки ресурсов - одна из самых сложных частей API. Если не продумать их заранее, позже придётся ломать клиентов.

Постраничная выдача

Постраничная выдача нужна всегда, когда список может быть большим. Есть два основных подхода:

  1. Смещение (offset/limit):

    • Простой в реализации
    • Неэффективен для больших смещений
    • Пример: /tasks?offset=20&limit=10
  2. Курсор:

    • Эффективнее для больших объёмов данных
    • Сложнее в реализации
    • Пример: /tasks?cursor=abc123&limit=10

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

Фильтры

Фильтры позволяют клиенту получать только нужные данные. Например:

  • /tasks?status=open - задачи со статусом “открыто”
  • /tasks?assignee=123 - задачи, назначенные пользователю с ID 123

Фильтры должны быть простыми и предсказуемыми. Не стоит делать сложные фильтры с логическими операциями - это усложняет реализацию и использование.

Сортировка

Сортировка позволяет клиенту получать данные в нужном порядке. Например:

  • /tasks?sort=due_date - сортировка по дате выполнения
  • /tasks?sort=-priority - сортировка по приоритету (по убыванию)

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

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

Идемпотентность - это свойство операции, при котором повторное выполнение не меняет результат. Например, удаление задачи с ID 123 идемпотентно: если задача уже удалена, повторное удаление не изменит состояние системы.

Почему это важно:

  1. Сеть может потерять запрос
  2. Сервер может упасть во время обработки
  3. Клиент может не получить ответ

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

Как сделать операцию идемпотентной:

  1. Использовать идемпотентные методы HTTP (GET, PUT, DELETE)
  2. Добавлять идемпотентность на уровне бизнес-логики (например, использовать client_id для предотвращения дубликатов)
  3. Использовать заголовок Idempotency-Key

Права доступа: что остаётся за рамками API

Схема API определяет, какие данные доступны и как к ним обращаться. Но она не решает вопрос прав доступа к конкретной записи.

Например, пользователь может иметь доступ к списку задач (GET /tasks), но не к конкретной задаче (GET /tasks/123), если она принадлежит другому проекту.

Права доступа - это бизнес-логика, которая должна быть реализована на сервере. Клиент не должен знать, какие записи ему доступны - он просто делает запрос, а сервер решает, вернуть данные или ошибку 403 Forbidden.

Как реализовать права доступа:

  1. Проверка прав на уровне маршрута (например, при запросе GET /tasks/123)
  2. Фильтрация данных на уровне запроса (например, при запросе GET /tasks)

Позиция редакции

Проектирование REST API - это про контракты между сервером и клиентами. Контракт должен быть стабильным, предсказуемым и удобным для всех сторон.

Основные принципы:

  1. Сущности и их отношения должны определяться первыми. Маршруты - это лишь способ доступа к данным.
  2. Коды ответов - часть контракта. Неправильное использование кодов ломает всю инфраструктуру между клиентом и сервером.
  3. Форма ошибки должна быть единообразной. Клиент должен уметь обработать любую ошибку.
  4. Изменения API должны быть совместимыми. Добавлять можно, переименовывать нельзя, удалять - только с предупреждением.
  5. Списки должны поддерживать постраничную выдачу, фильтры и сортировку.
  6. Операции должны быть идемпотентными. Клиент должен уметь безопасно повторять запросы.
  7. Права доступа - это бизнес-логика, которая не решается на уровне API.

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

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

С чем это связано

Метка над заголовком говорит, зачем туда идти: продолжить тему, перейти к практике или увидеть возражение.

Контракт существует, только если записан отдельно от кода обеих сторонполе вне схемы
Предыстория

JSON как контракт: где он подводит

JSON как контракт — почему его недостаточно для надёжного обмена данными и что документировать отдельно.

Сравнение путей пагинации: медленный и быстрый вариантыДолгое смещениеБыстрый курсор
Практика

Постраничная выдача: смещение против курсора

Как выбрать между смещением и курсором для пагинации в API — и не потерять данные или скорость на больших объёмах.

Изменение схемы пробивает границу контракта и зажигает сигнал, останавливающий деплой до того, как сломается отчётграница контрактаизменение схемы
Углубление

Контракты данных: как перестать чинить отчёты

Как наладить работу с данными, чтобы не переписывать отчёты после каждого изменения API — и с чего начать внедрение контрактов.

Письмо по вторникам

Один разбор недели и короткий список того, что изменилось. Без дайджестов на сорок ссылок.

Начните вводить — материалы появятся здесь.

выбрать · Enter открыть · Esc закрыть