REST API: как спроектировать правильно и не наломать дров

mr. Cooper 1 час назад Веб-разработка
REST API: как спроектировать правильно и не наломать дров

Представьте: вы запускаете свой REST API в прод, а через неделю он падает под нагрузкой. Клиенты жалуются на тормоза, разработчики мобильного приложения пишут, что «ваше API - это какой-то ад», а вы сидите и не понимаете, где ошиблись. Знакомо?

Если вы не хотите оказаться в этой ситуации - эта статья для вас. Я не буду пересказывать теорию из Википедии. Вместо этого я проведу вас через реальные грабли, которые я и мои коллеги собирали годами. Вы узнаете, как проектировать REST API, который не ломается, быстро работает и заставляет разработчиков говорить «вау, какое удобное API». Мы разберём конкретные примеры кода, сравним подходы и я покажу вам приёмы, которых нет в официальной документации.

Меня зовут Александр, я ведущий разработчик с 8-летним опытом, и за моими плечами десятки API - от маленьких стартапов до систем с миллионной аудиторией. Всё, что я здесь пишу, я выстрадал на реальных проектах.

Что такое REST API (и почему все вокруг только о нём и говорят)

Давайте сразу договоримся: REST API - это не технология, а архитектурный стиль. Как стиль в архитектуре зданий: можно построить дом в стиле хай-тек, а можно в классическом. Так и здесь - вы можете спроектировать ваше API по правилам REST, а можете сделать всё "как бог на душу положит". REST - это просто набор проверенных правил, которые делают ваше API удобным и надёжным.

Суть этих правил проста. Вы делите вашу систему на ресурсы - пользователи, товары, заказы. Каждому ресурсу даёте адрес (URL) и описываете, что с ним можно делать через стандартные HTTP-глаголы: взять (GET), создать (POST), заменить (PUT), обновить (PATCH), удалить (DELETE). И ещё одно важное правило: сервер не запоминает, кто вы и что делали пять минут назад. Каждый запрос самодостаточен. Это называется stateless (отсутствие состояния), и именно благодаря этому REST-системы можно масштабировать на сотни серверов.

Но теория - это скучно. Давайте сразу перейдём к тому, где люди реально ошибаются.

Почему 90% REST API - это боль (и как не стать частью этой статистики)

Скажу вам как человек, который принимал участие в ревью сотен чужих API. В 9 случаях из 10 разработчики совершают одни и те же ошибки. И вот самые живучие из них.

Ошибка №1: REST API как RPC. Разработчики приходят из мира SOAP или внутренних вызовов и начинают делать URL вида /getUserById или /createOrder. Это не REST. REST оперирует ресурсами, а не действиями. Правильно: GET /users/123, POST /orders. Запомните: ресурсы - существительные, а не глаголы.

Ошибка №2: "А зачем нам статусы? Мы и так всё в теле ответа передадим". Я видел API, которые на любой запрос возвращали 200 OK, а ошибки писали в поле error внутри JSON. Это кошмар для клиентов. Представьте, что вы вызываете метод, парсите JSON, а там вместо данных - ошибка. Вы должны разбирать тело, чтобы понять, что пошло не так. Вместо этого используйте статусы: 404 - не найдено, 400 - плохой запрос, 403 - нет прав. Это стандарт, который понимают все языки и библиотеки.

Ошибка №3: Игнорирование пагинации. На стартапе три пользователя и пять товаров. Кто-то пишет GET /products и отдаёт весь список. Через год товаров 50 000, и этот запрос убивает базу, потому что нужно выбрать всё, сериализовать в JSON и отдать. Всё это время клиент будет ждать и висеть. Пагинация должна быть с самого начала, даже если у вас пять товаров. Используйте параметры ?page=1&limit=20 или, если у вас сложные данные, ?cursor=....

Ошибка №4: Отсутствие версионирования. Вы выпустили API. Через месяц оказалось, что поле user_name нужно переименовать в fullName. Вы это сделали, и все клиенты сломались. Потому что у API нет версии. Правило железное: версионируйте с первого дня. Варианта два: либо в URL /api/v1/users), либо в заголовке (через Accept-Version). Я рекомендую URL - это нагляднее, и это видно в логах.

Ошибка №5: Безопасность "на потом". "Сначала сделаем функционал, а защиту добавим позже". Знакомо? Потом никогда не наступает. JWT, OAuth2, HTTPS - это должно быть с первой строчки кода. Иначе вы получите утечку данных или, что ещё хуже, ваш API будут использовать боты для рассылки спама с вашего же сервера.

Как я проектирую REST API: пошаговый алгоритм от практика

Я прошёл путь от начинающего разработчика, который писал API как попало, до тимлида, который учит команду делать правильно. Вот мой личный чек-лист, которым я пользуюсь каждый раз, когда начинаю новый проект.

Шаг 1. Садимся и выписываем ресурсы. Это главный шаг. Если вы ошибётесь на нём, всё остальное будет кривым. Для интернет-магазина ресурсы будут: users, products, orders, reviews, cart. Не больше 5-7 ресурсов на один сервис. Если у вас больше - возможно, вы пытаетесь сделать монолит, а не REST API.

Шаг 2. Рисуем URL-структуру. Используем множественное число. GET /users - список, POST /users - создание, GET /users/123 - конкретный пользователь. Для связей используем вложенность, но не глубже двух уровней. Например, GET /users/123/orders - заказы конкретного пользователя. Но никогда не делайте GET /users/123/orders/456/products/789 - это уже перебор.

Шаг 3. Определяем, что возвращать. Здесь есть золотое правило: всегда возвращайте объект, а не массив на верхнем уровне. Плохо: [{...}, {...}]. Хорошо: {"data": [{...}, {...}], "pagination": {...}}. Так вы сможете добавить мета-информацию позже, не ломая клиентов.

Ещё один мой лайфхак: для списков всегда возвращайте не только массив, но и общее количество записей total), даже если вы их не показываете на фронтенде. Это помогает клиентам строить пагинацию и не делать лишних запросов для подсчёта.

Шаг 4. Проектируем фильтрацию и сортировку через query-параметры. Это стандарт. GET /products?category=laptops&min_price=30000&sort=price_asc. Сложные фильтры (например, "или" условия) делайте через специальный синтаксис. Я использую нотацию filter[field]=value, но это уже вкусовщина.

Шаг 5. Выбираем правильный метод для обновления. Тут кроется главная ловушка. PUT заменяет весь ресурс целиком. Если вы отправите PUT с {"name": "Иван"}, а у пользователя ещё есть поле email, то email станет null. Это неожиданно и опасно. Поэтому в 90% случаев используйте PATCH - он обновляет только переданные поля. Но даже PATCH бывает разным. Существует JSON Patch (RFC 6902) и JSON Merge Patch (RFC 7396). Я рекомендую JSON Merge Patch - он проще и покрывает 95% сценариев.

Шаг 6. Добавляем документацию через Swagger/OpenAPI. Это не для галочки. Это для того, чтобы вы сами не забыли, как работает ваше API через полгода. Я пишу документацию параллельно с кодом - это занимает на 10% больше времени, но экономит 50% времени на поддержку.

Шаг 7. Закладываем rate limiting. Ограничьте количество запросов с одного IP или по токену. Я обычно ставлю 100 запросов в минуту для бесплатных тарифов и 1000 для платных. Это защищает от ботов и от неаккуратных клиентов, которые в цикле шлют запросы.

Сравнение подходов: Offset-пагинация против Cursor-based

Вот тема, которую редко раскрывают в статьях для новичков, но она критична для больших проектов.

Offset-пагинация ?page=2&limit=20): простая, интуитивная, работает с любой базой. Но у неё есть проблема: если вы перейдёте на страницу 100, база данных всё равно будет пролистывать 2000 записей, чтобы отдать вам следующие 20. Это медленно. И ещё одна беда: если во время пагинации кто-то добавил новый товар в начало списка, то страницы "съедут" - вы получите дубликаты или пропуски.

Cursor-based пагинация ?cursor=eyJpZCI6IDQyfQ&limit=20): более сложная в реализации, но она работает быстро на больших объёмах и не даёт сдвигов. Идея в том, что вы передаёте курсор - указатель на последнюю запись, которую вы видели. Сервер ищет записи, которые идут после этого курсора. Это работает за O(log N) даже на миллионах записей.

Мой совет: если у вас больше 10 000 записей и вы пишете публичное API - используйте cursor-based. Если у вас маленький проект - используйте offset, он проще и понятнее клиентам.

Примеры кода: от простого к сложному

Давайте разберём не просто абстрактные примеры, а реальную боль. Представьте, что у нас есть интернет-магазин, и мы делаем эндпоинт для получения заказов пользователя с фильтрацией по статусу и дате.

Плохой вариант (то, что часто пишут новички):

GET /api/v1/getOrdersByUser?user_id=123&status=paid&from=2026-01-01

Почему плохо:

  • В URL глагол getOrdersByUser - это не REST.

  • user_id в параметрах - ресурс должен быть в пути, а не в параметре.

  • Нет версионирования - если мы изменим логику, клиенты сломаются.

Хороший вариант (правильный REST + фильтрация):

GET /api/v1/users/123/orders?status=paid&from=2026-01-01&sort=created_at_desc&page=2&limit=20

Пример ответа (с пагинацией и мета-информацией):

{
  "status": "success",
  "data": [
    {
      "id": "order_789",
      "total": 15000,
      "status": "paid",
      "created_at": "2026-06-15T10:30:00Z",
      "items": [
        {"product_id": 42, "name": "Ноутбук", "quantity": 1, "price": 15000}
      ]
    }
  ],
  "pagination": {
    "page": 2,
    "limit": 20,
    "total": 156,
    "pages": 8
  },
  "links": {
    "first": "/api/v1/users/123/orders?page=1&limit=20",
    "prev": "/api/v1/users/123/orders?page=1&limit=20",
    "next": "/api/v1/users/123/orders?page=3&limit=20",
    "last": "/api/v1/users/123/orders?page=8&limit=20"
  }
}

Обратите внимание на поле links - это часть концепции HATEOAS, о которой я расскажу дальше. Даже если вы не используете её полностью, ссылки prev/next очень облегчают жизнь клиентам.

HATEOAS: почему о нём все говорят, но никто не использует

HATEOAS - это самый сложный и самый спорный принцип REST. Идея в том, что сервер возвращает не просто данные, а ссылки на действия, которые можно сделать с этими данными. Например, после создания пользователя вы возвращаете не только его ID, но и ссылку на его профиль, ссылку на сброс пароля, ссылку на удаление.

{
  "data": {
    "id": 123,
    "name": "Иван"
  },
  "links": {
    "self": "/api/v1/users/123",
    "update": "/api/v1/users/123",
    "delete": "/api/v1/users/123",
    "orders": "/api/v1/users/123/orders",
    "reset_password": "/api/v1/users/123/reset"
  }
}

Это делает API самоописываемым. Клиент не должен знать, как собрать URL для удаления - он просто берёт ссылку из ответа. Красиво, правда?

Но на практике HATEOAS почти не используется. Почему? Во-первых, это сильно раздувает ответы. Во-вторых, мобильные разработчики всё равно хардкодят URL, потому что им нужно точно знать, куда отправлять запросы. В-третьих, это сложно поддерживать - при изменении структуры вы должны обновлять все ссылки.

Моя рекомендация: используйте HATEOAS только для ссылок пагинации prev/next) и для "self" - ссылки на сам объект. Этого достаточно, чтобы API было удобным, но не переусложнённым.

Безопасность REST API: как мы потеряли данные клиента и что из этого вынесли

Расскажу реальную историю. На одном из моих первых проектов мы не добавили rate limiting. Через неделю после запуска наш API начал тормозить. Мы смотрим логи - какой-то бот долбит эндпоинт /products с частотой 1000 запросов в секунду. Он не крал данные, он просто тестировал своё приложение в бесконечном цикле. Но наша база не выдержала, и мы упали на два часа. Теперь ограничение запросов я добавляю на этапе проектирования - как и HTTPS, и JWT-аутентификацию.

Вот мой минимальный набор безопасности для REST API:

1. HTTPS всегда. Даже в разработке используйте самоподписанные сертификаты. Это предотвращает MITM-атаки и защищает токены.

2. Аутентификация через JWT или OAuth2. JWT проще - вы выдаёте токен, клиент его хранит и отправляет в заголовке Authorization: Bearer <token>. Обязательно ставьте время жизни токена (обычно 15-60 минут) и делайте refresh-токены для продления сессии.

3. Проверяйте права на каждый эндпоинт. Пользователь с ролью "гость" не должен видеть админские эндпоинты. Это кажется очевидным, но мы пропустили это в одном проекте, и пользователи могли менять цены товаров. Да, был скандал.

4. Валидация входных данных. Не доверяйте клиенту. Проверяйте типы, длины, форматы (email, телефон). Используйте библиотеки валидации (Joi, Zod, Pydantic), чтобы не писать валидацию вручную.

5. Ограничение частоты запросов (rate limiting). Напишите middleware, который считает запросы с IP или по пользователю. Я использую Redis для хранения счётчиков - это быстро и масштабируемо.

6. Защита от инъекций. Используйте параметризованные запросы в SQL (никакой конкатенации строк!). Для NoSQL тоже есть свои методы экранирования.

Часто задаваемые вопросы (FAQ)

Вопрос: Я слышал, что REST умирает, все переходят на GraphQL. Стоит ли мне учить REST в 2026 году?

Однозначно да. REST никуда не умирает. GraphQL - это мощный инструмент для сложных выборок, но он в разы сложнее в реализации, кэшировании и защите. REST - это "рабочая лошадка". По статистике, более 80% публичных API всё ещё используют REST. Изучайте и то, и другое, но начинать лучше с REST.

Вопрос: Что делать, если мне нужно передать не JSON, а файл? Можно ли через REST?

Да, используйте multipart/form-data в POST или PUT запросах. В ответе вы можете вернуть JSON с ID загруженного файла. Это стандартный подход.

Вопрос: Как правильно обрабатывать ошибки валидации?

Возвращайте статус 400 Bad Request и в теле ответа показывайте, какие поля не прошли валидацию и почему. Я использую формат:

{
  "status": "error",
  "code": "validation_failed",
  "message": "Проверьте правильность введённых данных",
  "errors": {
    "email": "Неверный формат email",
    "password": "Пароль должен содержать минимум 8 символов"
  }
}

Это позволяет фронтенду подсветить конкретные поля с ошибками.

Вопрос: GET запрос должен быть идемпотентным, но мой GET что-то там обновляет (например, счётчик просмотров). Как быть?

Счётчик просмотров - это побочный эффект. Спецификация говорит, что GET должен быть безопасным, но допускает незначительные побочные эффекты (логирование, подсчёт). Если вы обновляете серьёзные данные через GET - это архитектурная ошибка. Вынесите это в POST или PUT.

Вопрос: У меня есть сложные связи, например, пользователь -> заказы -> товары -> отзывы. Как мне получать всё сразу, чтобы не делать 5 запросов?

В REST для этого нет встроенного механизма (в отличие от GraphQL). Но вы можете добавить параметр ?include=orders.items.reviews, и сервер вернёт вложенную структуру. Это называется "расширение полей" (field expansion) или "embedding". Используйте с осторожностью - такие запросы могут быть тяжелыми для базы.

Вопрос: Как мне протестировать REST API?

Юнит-тесты (ваша логика), интеграционные тесты (запросы к реальной базе) и E2E тесты (полный сценарий). Для ручного тестирования используйте Postman или Insomnia. Для автоматизированных - библиотеки вроде Supertest (Node.js), pytest (Python) или RestAssured (Java). Обязательно пишите тесты на каждый эндпоинт.

Вопрос: Что такое OpenAPI и Swagger? Они мне нужны?

Swagger - это набор инструментов для работы с OpenAPI. OpenAPI - это спецификация для описания вашего API в формате YAML или JSON. Это нужно для документации, для генерации клиентских SDK и для автоматического тестирования. Я настоятельно рекомендую использовать OpenAPI на любом проекте, даже на маленьком.

Полезные советы и лучшие практики

Теперь, когда мы прошли теорию, практику и ошибки, я дам вам свой личный "кодекс" разработчика REST API:

1. Проектируйте API как публичный, даже если он приватный. Это сделает ваш код чище, а если проект вырастет - вы уже будете готовы.

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

3. Наблюдайте за метриками. Какие эндпоинты самые медленные? Какие вызывают чаще всего? Где больше всего ошибок? Это даст вам пищу для оптимизации.

4. Пишите Readme для своего API. Даже если у вас есть Swagger, короткий текст "как быстро начать" с примерами запросов экономит время вам и клиентам.

5. Обновляйте API постепенно. Если вам нужно сделать breaking change - заведите новую версию /v2/...) и дайте клиентам время на миграцию (обычно 3-6 месяцев). Потом старую версию можно деактивировать.

6. Используйте инструменты мониторинга. Я использую Sentry для ошибок и Prometheus + Grafana для метрик. Это помогает находить проблемы до того, как о них скажут пользователи.

7. Деплойте сначала на staging, потом на production. Это банальность, но я видел проекты, где разработчики деплоили на прод в 2 часа ночи и ломали всё. Сделайте CI/CD с тестами - это окупится.

8. Будьте дружелюбны к клиентам. Если у клиента ошибка - возвращайте понятное сообщение, а не просто {"error": "Internal Server Error"}. Объясняйте, что пошло не так. Люди любят API, которые говорят на понятном языке.

Итог

REST API - это не rocket science. Это просто набор правил, которые помогают строить устойчивые, масштабируемые системы. Мы разобрали:

  • Что такое REST API и почему он стал стандартом.

  • Самые частые ошибки и как их избегать.

  • Мой личный пошаговый алгоритм проектирования.

  • Когда использовать offset, а когда cursor-based пагинацию.

  • Примеры реального кода от простого к сложному.

  • Честный разговор о HATEOAS - когда он нужен, а когда нет.

  • Как не потерять данные и защитить своё API.

  • Ответы на 7 самых популярных вопросов.

Я хочу, чтобы вы запомнили главное: REST API - это интерфейс, который вы даёте людям. Относитесь к нему как к лицу вашего сервиса. Сделайте его красивым, понятным и надёжным. И тогда ваши пользователи (и разработчики, и конечные клиенты) скажут вам спасибо.

Комментарии

Пока нет комментариев. Будьте первым, кто напишет.

Чтобы оставить комментарий, войдите в аккаунт.

Похожие статьи