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

Сервис принимает заказ с корректным JSON, но отклоняет его только после записи части данных в базу. Или наоборот: API возвращает клиенту «неверный запрос», хотя проблема в бизнес-правиле — товар закончился. Такие сбои появляются, когда валидацию складывают в один слой по принципу «где удобнее». Для HTTP API ориентиром здесь служит RFC 9110, а для приложений с реляционной базой — тот же подход применим, например, к PostgreSQL 16: проверка должна стоять там, где известен нужный контекст.
Валидация — это не одна проверка
Под одним словом обычно смешивают несколько разных задач. От этого и возникает спор: разработчик API считает, что всё проверил, а бизнес-логика или база данных всё равно отклоняет операцию.
- Синтаксическая проверка — запрос можно разобрать: JSON не повреждён, поле имеет ожидаемый тип, дата записана в допустимом формате.
- Проверка контракта — обязательные поля переданы, строка укладывается в длину, число находится в допустимом диапазоне.
- Бизнес-валидация — заказ можно оформить именно сейчас: клиент активен, лимит не исчерпан, переход статуса разрешён.
- Проверка инвариантов — данные не нарушают правила, которые должны сохраняться всегда: уникальность email, положительный остаток, существующая связь между сущностями.
Первые два пункта обычно относятся к входному сообщению. Последние два требуют состояния системы и не должны зависеть только от формы конкретного HTTP-запроса.
Валидация должна находиться не «поближе к данным», а на границе той ответственности, которая умеет доказать, что данные допустимы.
Инвариант — условие, которое должно оставаться истинным после любой успешной операции. Например, у заказа не может быть одновременно оплаченного статуса и отрицательной суммы.
Какие проверки принадлежат каждому слою
Граница API: формат и контракт
Контроллер, middleware или слой схемы запроса должен быстро отсекать сообщения, которые невозможно безопасно передать дальше. Здесь уместны проверка типов, обязательности, формата UUID, длины строки, допустимого перечисления и размера тела запроса.
Эта проверка не должна решать, можно ли пользователю купить товар или изменить статус документа. API знает структуру сообщения и сведения об аутентификации, но не обязан содержать весь сценарий предметной области.
Ошибку формата лучше возвращать до вызова базы данных и внешних сервисов. В HTTP API повреждённый или некорректно сформированный запрос обычно получает статус 400. Для корректно разобранного сообщения, которое нарушает семантические условия, контракт может использовать 422 Unprocessable Content; важно выбрать правило один раз и закрепить его в документации.
Прикладной слой: сценарий операции
Сервисный слой отвечает на вопрос «можно ли выполнить эту операцию с учётом текущего состояния системы». Он проверяет права на конкретное действие, доступность товара, лимит, допустимость перехода статуса и ограничения, зависящие от нескольких объектов.
Такую логику нельзя оставлять только в контроллере. Тот же сценарий может запускаться из очереди, административной панели, CLI-команды или другого сервиса. Если правило живёт только в HTTP-обвязке, второй путь обойдёт его.
Доменный слой: правила самой модели
Правило принадлежит доменной модели, когда оно должно выполняться при любом изменении конкретного объекта. Например, сумма позиции не бывает отрицательной, а оплаченный счёт нельзя вернуть в состояние «черновик» без отдельной процедуры отмены.
Доменный слой не должен знать о JSON, HTTP-кодах и названиях полей формы. Он принимает уже подготовленные значения и сообщает, почему операция нарушает инвариант. Способ отображения этой ошибки — задача прикладного или транспортного слоя.
База данных: последняя линия защиты
Ограничения базы нужны для правил, которые должны выдерживаться даже при гонке запросов, ошибке приложения или прямой служебной записи. Уникальные индексы, NOT NULL, CHECK и внешние ключи защищают данные там, где проверка в приложении уже не гарантирует результат.
База не должна превращаться в единственный движок бизнес-логики. Она плохо объясняет пользователю сложный сценарий и не знает, какой текст ошибки нужен мобильному клиенту. Но рассчитывать, что два параллельных запроса всегда успеют пройти одинаковую проверку в коде, опасно.
Три архитектурных подхода и их цена
| Подход | Где проверяют | Плюс | Цена и риск |
|---|---|---|---|
| Всё на входе | Контроллер или схема запроса | Простая трассировка ошибок, быстрый отказ | Очереди и фоновые задачи могут обойти правила |
| Всё в сервисах | Прикладной слой | Сценарии проходят через единый путь | Дублирование для разных операций и слабая защита от гонок |
| Всё в доменной модели | Сущности и value objects | Инварианты защищены независимо от транспорта | Контекстные правила и ошибки API всё равно требуют внешнего слоя |
| Защита в несколько уровней | API, приложение, домен и база | Каждый слой закрывает свой класс риска | Нужны единые правила ошибок и тесты без противоречий |
Для большинства систем подходит последний вариант, но не как копирование одной и той же проверки четыре раза. Например, формат UUID проверяется на границе, право на операцию — в сервисе, переход статуса — в домене, а уникальность номера — в базе. Повторная проверка оправдана, только если она защищает другую границу или другой сценарий.
Что часто ломается на практике
Проверка существования перед записью
Классическая ошибка: приложение сначала спрашивает, свободен ли уникальный ключ, а затем выполняет вставку. Между двумя запросами другой процесс может занять это значение. Проверка в коде улучшает сообщение для пользователя, но окончательное решение должно принимать ограничение базы, а приложение — корректно обработать конфликт.
Разные правила для разных входов
Веб-форма разрешает одно значение, импорт CSV — другое, а административная команда вообще не проверяет поле. Исправление — вынести общее бизнес-правило из транспорта и прогонять через него все каналы. Формат CSV при этом остаётся ответственностью импортера.
Проверка только на чтении
Старые записи могли быть созданы по прежним правилам, а внешняя система может прислать неожиданные данные. Нельзя считать, что объект безопасен только потому, что его однажды создала ваша версия приложения. На записи проверяйте инварианты, на чтении — корректно обрабатывайте несовместимые или неполные данные.
Слишком подробные ответы наружу
Текст исключения из базы может раскрыть имя таблицы, внутренний идентификатор или условие ограничения. Внешнему клиенту отдавайте стабильный код ошибки и понятное поле, а технические детали пишите в журнал с correlation ID. Это особенно важно для публичного API, где ответ становится частью долгоживущего контракта.
Как выбрать место для новой проверки
- Опишите условие одним предложением. Если в нём есть JSON, HTTP или форма, начните с транспортного слоя. Если есть «может ли этот пользователь» или «разрешён ли переход», это прикладное правило.
- Проверьте повторное использование. Если сценарий вызывается не только из контроллера, правило нельзя запирать в HTTP-коде.
- Оцените последствия гонки. Для уникальности, балансов и остатков нужна защита на уровне транзакции или ограничения базы, а не только предварительный SELECT.
- Разделите ошибку для человека и причину для системы. Клиенту нужен стабильный контракт, разработчику — диагностические данные, а не случайный текст исключения.
- Добавьте тест на границу. Проверяйте не только успешный сценарий, но и прямой вызов сервиса, параллельную запись и повторную доставку сообщения.
Что делать дальше
Возьмите одну операцию с несколькими входами — например, создание заказа — и составьте карту проверок: формат, права, бизнес-условия, инварианты и ограничения базы. Для каждой отметьте владельца и внешний код ошибки. Затем найдите дубли, которые дают разные результаты, и поставьте метрики: долю отклонённых запросов по причине, конфликты уникальности, ошибки constraint и повторные попытки очереди.
После этого отдельно проверьте транзакционные границы и идемпотентность. Инвалидация кеша и проверка прав доступа связаны с валидацией, но это отдельные темы: не смешивайте их в одну функцию только ради сокращения кода.
Проверка рекламы на риски: Что в макете вызовет вопросы ФАС: превосходные степени, сравнения, обещания результата, маркировка.
Спросить за 40 ₽Источники: RFC 9110: HTTP Semantics, Microsoft Azure Architecture Center: N-tier architecture
Частые вопросы
Нужно ли проверять одно и то же поле в контроллере и в сервисе?
Можно ли оставить бизнес-валидацию только в базе данных?
Какой статус возвращать при ошибке валидации: 400 или 422?
Что делать, если проверка прошла, но база всё равно вернула ошибку?
Нужно ли валидировать данные, которые пришли из очереди?
Где проверять права доступа: валидация это или отдельная задача?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.