единая форма ошибкилюбой отказ → RFC 9457 application/problem+json: {type, title, status, detail, code, request_id} (+ errors[] на 422); клиент читает машинный code, не парсит текст; форма одна на всех роутерах
бизнес-нарушение → 4xx, не 500нарушение правила отдаёт свой код (401 · 403 · 404 · 409 · 422 · 429), не проваливается в 500; 500 — только незапланированный сбой
словарь кодов размечен401 нет личности · 403 нет права · 404 нет ресурса (приватное маскируем под 404, не 403) · 409 конфликт/замок · 422 валидация тела · 429 лимит
время — UTC ISO-8601каждое datetime в ответе сериализуется как …Z (UTC); ни naive, ни локальная зона; пояс применяет фронт
версия в пути/api/v1 · /public/v1; без версии → 404; ломающее изменение → новая версия, старая жива
личность до обработчикаJWT или ключ ach_… резолвится в user_id → роль → ACL прежде входа в хендлер; нет личности → 401 единообразно на защищённых роутах
лимит частоты → 429 + Retry-Afterпревышение окна → 429 с заголовком Retry-After; тело — тот же envelope (rate-limit)
нет голых роутовпараметризация по app.routes: каждый защищённый роут отвечает 401 без личности — новый эндпойнт не проскочит мимо проверки