Привет всем, на связи снова Дарья Борисова, системный аналитик из ПСБ. Продолжаю развеивать мифы о REST API. Если вы пропустили первую и вторую часть, то советую заглянуть туда: ведь мы уже разобрали некоторые заблуждения о природе REST. Сегодня мы разберем нюансы транспортных и бизнес-ошибок, погрузимся в кеширование и узнаем, действительно ли REST должен быть прокси для базы данных.
Переходите под кат, начинаем!
Миф 1. HTTP-коды только для транспортных ошибок, бизнес-ошибки всегда 200.

Миф
HTTP-статусы (4xx, 5xx) — это только технические детали протокола. Они описывают исключительно процесс обмена данными между клиентом и сервером.
Например:
404 Not Found — запрошенный URL не существует
403 Forbidden — сервер понял запрос, но не разрешает доступ
500 Internal Server Error — сбой на стороне сервера
Из этого появляется вывод:
Все, что связано с бизнес-логикой (например, найден ли заказ, можно ли его отменить), должно передаваться только в теле ответа, а HTTP-статус почти всегда должен быть 200 OK (или иногда 400 Bad Request).
Реальность
В REST-подходе HTTP-статусы — это основной способ сообщить результат операции.
Они не второстепенны — они часть самого интерфейса.
Объяснение
1. REST — это не просто JSON по HTTP.
REST — это архитектурный стиль с четкими правилами. Одно из ключевых — единый интерфейс. HTTP уже реализует этот интерфейс, и статусы — его важная часть.
Они:
стандартизированы;
понятны программам;
работают «из коробки» для прокси, кэшей, API-шлюзов.
2. Деление на «технические» и «бизнес-» ошибки — искусственное
Для клиента ощутимой разницы нет:
«страница не найдена»;
«заказ не найден».
В обоих случаях результат один: запрошенный ресурс отсутствует. И код 404 идеально это передает — без дополнительных соглашений и костылей.
3. Что это значит для дизайна API
Вот как обычно правильно использовать статусы:
Если /api/orders/999 не существует — это 404 Not Found. Можно добавить JSON для вывода пользователю, но необязательно.
Пользователь авторизован, но не имеет прав — это 403 Forbidden. Четко разделяет:
кто ты? — 401;
можно ли тебе это? — 403.Конфликт с текущим состоянием ресурса — 409 Conflict. Например: заказ уже выполнен, email уже занят.
Ошибка в бизнес-правилах — 422 Unprocessable Entity. Запрос корректный по форме, но выполнить его нельзя. Например: дата окончания раньше даты начала.
4. Почему 200 OK для ошибок — плохая идея
Формат вроде:
{ "success": false, "error": "..." }
Это антипаттерн. Всё превращается в «что-то пошло не так»
Почему это плохо:
Ломает инфраструктуру.
Прокси, мониторинг и логирование не понимают, что произошла ошибка.Усложняет логику на клиенте.
Клиенту приходится проверять HTTP-статус и поле в JSON.Стирает различия между ситуациями.
Нет разницы между: не найдено (404), нет прав (403), ошибка запроса (400).
Вывод
При проектировании RESTful API рассматривайте HTTP-статусы как первичный канал передачи состояния операции в рамках домена. Ваша задача — сопоставить бизнес-сценарии («пользователь не найден», «заказ уже оплачен», «лимит исчерпан») с наиболее подходящими, семантически богатыми кодами состояния из стандарта HTTP. Это не «загрязнение» протокола бизнес-логикой, а, наоборот, правильное использование предоставленного стандартом «словаря» для создания понятного и эффективного интерфейса.
Миф 2. REST API должен быть тонким слоем над БД.

Миф
REST API должен быть тонким слоем над базой данных: один HTTP-запрос — одна операция CRUD над таблицей. И если у нас только CRUD-операции, то REST API неизбежно становится тонким слоем над базой данных.
Реальность
REST — это про работу с ресурсами через интерфейс, а не требование отражать структуру базы данных. API может и часто должен содержать полноценную бизнес-логику, но при этом CRUD-операция не должна раскрывать структуру хранения
Объяснение
Этот миф возникает из-за упрощённого понимания REST как «обёртки над CRUD». Действительно, базовые операции (создание, чтение, обновление, удаление) удобно маппятся на HTTP-методы (POST, GET, PUT/PATCH, DELETE). Однако это лишь частный случай, а не цель архитектуры.
Как понять, что ваши методы — это «прокси для БД»?
Клиенту за один запрос отдаются все столбцы одной таблицы, в том числе технические поля. Нет агрегации данных из нескольких таблиц/систем.
Вся бизнес-логика либо не прописана либо вынесена на клиента.
API жёстко повторяет структуру базы данных, и любое изменение схемы БД ломает внешний контракт.
Невозможны разные представления одного ресурса (например, краткое и полное).
Чем грозит «тонкий слой»:
утечка внутренней структуры данных наружу;
дублирование логики на клиентах;
сложности с версионированием;
рост технического долга при изменении предметной области.
Как исправить?
Работать с ресурсами, а не таблицами.
Один ресурс ≠ одна таблица. Т.е. клиент должен получать нужные ему данные, а не те, которые удобны серверу.Встраивать бизнес-логику в обработку метода.
Неважно, будет она в коде сервиса или в самой БД в качестве процедуры, главное — чтобы на стороне сервера.Проверять инварианты.
То есть, соблюдение условия, что действие над ресурсом является разрешенным и легитимным.Не бояться выходить за рамки обычного CRUD.
Иногда честнее сделать:
POST /заказы/{id}/подтверждение
POST /заказы/{id}/отмена
чем:
PATCH с полем «статус»: «подтверждён»
Вывод
REST API — это не отражение базы данных, а контракт уровня предметной области. Чем дальше API от структуры хранения и ближе к бизнес-смыслу ресурсов и операций, тем он устойчивее, безопаснее и проще в развитии.
Миф 3. Кеширование в REST — это про заголовки HTTP и всё.

Миф
На практике многие команды считают, что они «поддерживают кеширование», если добавили Cache-Control и ETag.И на этом всё заканчивается.
Реальность
На деле мы имеем большую градацию способов. Рассмотрим подробно.
Объяснение
Для client-server
Цель кеширования:
Снижение связности (клиент может вообще не ходить на сервер за данными);
Ответственность за данные переносится за рамки сервиса. Сервер перестаёт быть единственной точкой истины в момент запроса. Появляется «согласованность в итоге» на уровне HTTP;
Снижение задержек при ответе клиенту.
В чем ошибка большинства:
все ответы помечаются как no-store;
все ответы одинаково кешируются.
Не учитывается, что все ресурсы имеют разную природу, скорость и модель изменения.
Что делать для вызовов типа client-server?
Не игнорировать If-None-Match и If-Modified-Since. Они помогут уменьшить нагрузку без потери актуальности
(самое сложное) Определить стратегию инвалидации кеша (когда, кто, как синхронизироваться?)
А что происходит в вызовах типа server-server?
Обычно команды боятся: «А вдруг пользователь увидит неактуальные данные?» И выбирают Cache-Control: no-store. В итоге те самые слабосвязанные микросервисы теряют это волшебное свойство слабой связности. А еще встает вопрос кто отвечает за инвалидирование кеша? Ответ обычно: никто.
И если все-таки ответственного за инвалидацию кеша нашли, Cache-Control: no-store исправили, то встает новый вопрос:
«Зачем нам кеш, если вызовы идут по внутренней сети с минимальной задержкой? Для нас кеш — источник багов, а не оптимизации».
Но это не так. Микросервисы, несмотря требования, что данные обновляются мгновенно и всегда остаются актуальными, усиливают потребность в кеше, позволяя системе масштабироваться с меньшими затратами.
Как сделать «зрелый» кеш для server-server?
Проектировать кешируемость на уровне контракта: определение TTL и различие типов ресурсов;
Принять тот факт, что данные не могут быть всегда актуальными и определить степень актуальности, ввести SLA на свежесть. Проработать измеримость данного признака;
Ввести многослойный кеш (это уже не только на уровне REST: на гейтвее, на уровне сервиса);
Комбинировать с событиями, т. е. события инвалидируют кеш или обновляют модель чтения.
Вывод
Кеширование в REST — это не про Cache-Control, а про готовность системы жить с устаревшими данными ради снижения связности. Не отказывайтесь от кеша только потому, что вам лень управлять консистентностью.
____
На этом я завершаю цикл статей о мифах REST. Надеюсь, вам было интересно. Делитесь в комментариях впечатлениями.
Комментарии (15)

Rsa97
05.05.2026 06:53Если /api/orders/999 не существует — это 404 Not Found. Можно добавить JSON для вывода пользователю, но необязательно.
А если /api/book/350/page/12 вернул 404, то чего именно не существует - книги с id 350, страницы 12 в этой книге или вообще api? А если 403, то на что именно у клиента нет разрешения - на доступ к книге вообще, к конкретной странице или к операции (PUT/PATH/DELETE)?
Клиенту приходится проверять HTTP-статус и поле в JSON.
Но, чтобы понять, что именно произошло, клиенту и так придётся откуда-то эту информацию брать.
Одно из ключевых — единый интерфейс. HTTP уже реализует этот интерфейс, и статусы — его важная часть.
А что делать, если мы хотим использовать REST одновременно и по HTTP и по WebSocket? У последнего никаких статусов нет. Что в таком случае означает “единый интерфеейс”?

whoisking
05.05.2026 06:53А если /api/book/350/page/12 вернул 404, то чего именно не существует - книги с id 350, страницы 12 в этой книге или вообще api?
А какая разница? У вас хождение в глубину, чтобы юзеру попасть на page 12 надо сперва попасть на book 350. Для SEO вы будете генерировать sitemap и это ваша задача держать его обновлённым, там в целом тоже без разницы почему ссылка не валидна. Для кого это необходимо, кто целевая аудитория этого функционала?

Rsa97
05.05.2026 06:53Да читатель этой библиотеки. Что ему сказать при такой ссылке - “нет книги” или “нет страницы”?

ws233
05.05.2026 06:53Нет страницы? Даже если нет книги, то страницы-то тоже нет. В чем противоречие?

Rsa97
05.05.2026 06:53В точности описания ошибки. Если вам вернулось “нет такой страницы” вы, скорее всего, будете проверять, а есть ли другие страницы. А если “нет такой книги”, то и смысла искать другие страницы просто нет.

ws233
05.05.2026 06:53Это 2 разных уровня обработки с разной детализацией. Они не взаимоисключающие. Наоборот...
Реализация на основе HTTP-кодов может быть одна универсальная на все приложение. Да, будут проблемы с точностью описания ошибки. Зато дешево и сердито.
Если нужно дорого и детально прорабатывать, то пожалуйста - можно подниматься на уровень выше (или спускаться на уровень ниже, смотря как располагать уровни) и детализировать.
В этом смысл уровней. Есть один универсальный обработчик, если он не устраивает, дописываете конкретные обработчики на более высоких (низких уровнях) уровнях. За доп.деньги. При этом универсальный код все так же продолжает работать, если вдруг Вы забыли написать более конкретные вещи…
Поэтому заменять обработку HTTP-кодов джейсоном всегда – в корне неверно. Вы заранее обрекаете себя всегда на излишние затраты. Наоборот, правильно делать общие обработчики и потом их дополнять деталями, если это кому-то будет нужно и кто-то это оплатит...

daskuncik Автор
05.05.2026 06:53Привет!
Спасибо за комментарий.
Для случая /api/book/350/page/12 одной ошибки 404 точно будет мало, тк влечет неопределенность, которую мы дополняем типизацией ошибки в теле ответа. а Для случая /api/orders/999 отвечт не будет двойственным, поэтому тело можно не прикреплять.
Ошибка 403 дословно читается как "я понял, кто вы, но вам туда нельзя". Тип запрета зависит от вашего сервиса: если у вас книжный магазин, то покупателю нельзя производить никакие изменения в книгах, поэтому дополнительных пояснений к ошибке 403 не понадобится. А если у вас портал, где есть читатели и авторы, которые могут редактировать свое, а чужое не могут, тогда вас спасет описание ошибки в теле.
Про "клиенту приходится проверять и статус, и поле в теле". Здесь скорее о простых случаях, когда статус МОЖЕТ сказать сам за себя и доп.поле не требуется. В моей практике такие случаи нечасты.
При использовании HTTP и WebSocket понятие единого интерфейса не применимо
Rsa97
05.05.2026 06:53Ну так если мы всё равно добавляем поле с ошибкой в ответ и оно более информативно, чем статус HTTP, то есть смысл для унификации всегда добавлять такое поле. И не гадать, где оно есть, где его нет.
если у вас книжный магазин, то покупателю нельзя производить никакие изменения в книгах, поэтому дополнительных пояснений к ошибке 403 не понадобится
А если это был GET, то куда ему нельзя, в книгу вообще или именно на 12 страницу?
При использовании HTTP и WebSocket понятие единого интерфейса не применимо
А вот если использовать свою систему ошибок, не полагаясь на протокол, то единый интерфейс вполне себе возможен. Просто в WebSocket будет небольшой оверхед на id запроса и глагол (действие). А вся остальная часть будет той же самой.

ws233
05.05.2026 06:53А вот если использовать свою систему ошибок, не полагаясь на протокол, то единый интерфейс вполне себе возможен. Просто в WebSocket будет небольшой оверхед на id запроса и глагол (действие). А вся остальная часть будет той же самой.
Как-будто и используя систему ошибок HTTP тоже можно...
Вы ж просто расширите WebSocket транспортными ошибками, нет? Их можно придумать свои, а можно взять готовые из HTTP – в чем разница-то?
Как-будто это и есть рабочая, правильная и рекомендованная схема. Ошибки получаются разделенными на 2 уровня - транспортные и логические. При этом они могут быть использованы с любым транспортом (при расширении транспорта, не поддерживающего эти ошибки). Сами ошибки в такой схеме тоже генерируются и обрабатываются на двух разных уровнях и друг с другом не смешиваются, что хорошо.
Как-будто Вы с автором статьи говорите ровно об одном и том же, но разными словами.

Rsa97
05.05.2026 06:53Как-будто и используя систему ошибок HTTP тоже можно…
Можно, но недостаточно информативно. Скажем в ответ на GET /api/book/350/page/12 вам вернулся HTTP-ответ
403 Forbidden. К чему вам запрещён доступ - к API в целом, к книге или к странице?Вы ж просто расширите WebSocket транспортными ошибками, нет?
Зачем в WebSocket транспортные ошибки из HTTP, если достаточно использовать ошибки бизнес-логики?
Как-будто Вы с автором статьи говорите ровно об одном и том же, но разными словами.
Не совсем. Автор статьи предлагает использовать статусы HTTP и только если их недостаточно, то добавлять расширенную информацию. Я же за то, чтобы всегда использовать бизнес-ошибки, а транспортные ошибки оставить транспорту ну или, если хочется, использовать их параллельно.

ws233
05.05.2026 06:53Зачем в WebSocket транспортные ошибки из HTTP, если достаточно использовать ошибки бизнес-логики?
Вот тут ответ на этот вопрос. От общего к частному всегда дешевле, чем сразу все утыкивать частностями без необходимости...

ws233
05.05.2026 06:53Спасибо за статью.
Позволю себе добавить в миф про кеширование. На мобилах очень популярны библиотеки кеширования – огромное множество библиотек, которые слоем выше HTTP на клиенте полностью дублируют функционал, который уже имеется в HTTP из коробки. Например, SDWebImage для кеширования картинок на iOS. Для меня, например, такие библиотеки – сигнал, что команда (серверные и клиентские разработчики и аналитик) совершенно не проработала свой функционал. Соответственно, библиотеки эти из мобил надо выпиливать нещадно, а команду обучать лучшим практикам кеширования, встроенным в любую систему и сам протокол HTTP.
zo0Mx
я не знаю откуда эти "мифы" взяты и кто их считает таковыми, впервые слышу
whoisking
Я встречал немало любителей заворачивать всё в 200 ОК и придумывать кастомные ошибки для всего
Gromilo
Никто не запрещает использовать http только как транспорт, например https://en.wikipedia.org/wiki/JSON-RPC
Но это не REST.
Правда REST почти не бывает, ибо в апи почти ни у кого нет гипермедия