Для учебного сервиса ремонта велосипедов возьмём список заявок: клиент описывает поломку, мастер меняет статус, диспетчер закрывает карточку. CRUD здесь — не четыре кнопки, а правила для каждой операции и каждого повтора.
Карточка заявки и разрешённые действия
Минимальная запись: id, owner_id, title, status, created_at, updated_at, deleted_at. Статус ограничен значениями new, accepted, repairing, ready, closed. Имя владельца берётся из проверенной сервером сессии, а не из скрытого поля формы. Мастер видит назначенные заявки, диспетчер — очередь; это правила доступа к записи, не косметика интерфейса.
| Действие | Пример запроса | Проверка перед записью | Ожидаемый результат |
|---|---|---|---|
| Создать | POST /tickets |
заголовок непустой, статус задаёт сервер | новая заявка и её ID |
| Прочитать | GET /tickets/{id} |
пользователь имеет право видеть запись | карточка или отказ/404 согласно принятой политике |
| Изменить | PATCH /tickets/{id} |
разрешены только поля и переходы роли | обновлённая карточка |
| Удалить | DELETE /tickets/{id} |
право диспетчера; показать подтверждение | мягкое скрытие либо удаление по заранее выбранной политике |
Ограничения стоит закрепить и в базе: внешний ключ владельца, NOT NULL для обязательных полей, допустимый статус и индексы для часто используемой очереди. PostgreSQL описывает CHECK, NOT NULL, UNIQUE, primary и foreign keys как ограничения целостности; они защищают данные от некорректных записей независимо от формы (Constraints). Ограничение не заменяет проверку роли: база должна получить уже разрешённую операцию.
Повтор запроса и потерянный ответ
Представь: диспетчер нажал «Создать», сервер сохранил заявку, но сеть оборвалась до ответа. Повторный POST может создать вторую запись. Не назначай клиенту автоматический безусловный retry для создания. Один вариант — выдать клиенту уникальный идентификатор операции и в одной транзакции сохранить его вместе с результатом; повтор с тем же ключом возвращает прежний результат, а конфликтующий payload отклоняется. Это прикладная схема идемпотентности, которую нужно реализовать и проверить, а не обещание самого HTTP.
RFC 9110 определяет PUT и DELETE как идемпотентные по предполагаемому эффекту, а POST — не идемпотентный автоматически; клиент не должен повторять неидемпотентный запрос без отдельного знания, что это безопасно (RFC 9110, §9.2.2). Поэтому редактирование можно строить как PATCH с версией записи или PUT для полной замены; поведение повтора всё равно должно быть явным.
Для конкурентного редактирования добавь version или сравнение updated_at: если другой мастер уже изменил карточку, покажи конфликт и предложи перечитать данные, а не молча затри чужую правку.
Удаление — отдельное решение
Если заявка нужна для истории обслуживания, используй мягкое удаление: отметь deleted_at, исключи карточку из обычной очереди и оставь контролируемый способ восстановления. Если данные надо удалить окончательно, определи связанные записи, аудит и допустимый срок хранения до реализации. Не вызывай удаление через GET: чтение страницы не должно менять состояние. Диалог подтверждения помогает избежать случайного клика, но право проверяется сервером при самом запросе.
Запрос к генератору и проверяемый сценарий
Можно попросить модель: «Добавь endpoint создания заявки. Автор берётся из серверной сессии; разрешены только title и description; повторный operation_id возвращает исходный результат, но тот же ключ с другим содержимым даёт конфликт. Добавь проверки уникальности в БД и тесты для запрета чужого доступа. Покажи изменённые файлы и объясни транзакцию». Это постановка задачи, не исполнявшийся код.
Приёмка должна пройти как минимум четыре цепочки: создать → перечитать после обновления страницы; изменить статус допустимым переходом; повторить тот же запрос после имитации потерянного ответа и убедиться в одной записи; попытаться изменить или удалить чужую заявку и получить отказ. Отдельно проверь повтор с тем же ключом и другим содержимым, конкурентное редактирование и мягкое/окончательное удаление в соответствии с выбранной политикой.
CRUD-форма готова, когда интерфейс и сервер согласны о допустимых полях, роли проверяются на каждом изменяющем запросе, ограничения сохраняют целостность, а повторы и удаления дают заранее определённый результат. Созданный экран сам этого не доказывает.
