Тема
Фаза 0 — API-слой и OpenAPI-контракт
Статус: спека фазы 0; дополнена 2026-07-14 по итогам разборов Bitrix24 REST и Directus/Stripe/Shopify (идемпотентность, runtime-схема, batch, reconciliation — план реализации: волна 24 TZ.md). Детализирует API-слой ядра (концепция) до уровня «как именно»: версионирование, аутентификация, формат ответа, rate-limit, генерация OpenAPI. Стек и потребители по зонам — канонический стек. Автодокументация — часть DX для Claude Code. Универсальные правила подняты в студийную спеку specs/api.md.
Роль API в архитектуре
Ядро headless-ready, но не headless-first (anti-goal): публичный сайт рендерит сервер (Blade), а API — параллельный слой доступа к тем же данным для трёх потребителей:
| Потребитель | Что берёт | Аутентификация |
|---|---|---|
| React-острова (зона 1) | живой фильтр каталога, submission формы, точечные данные | публичный/CSRF (та же сессия) |
| Кабинет (зона 3, Inertia) | данные ЛК, конфигуратор, сквозное состояние | сессия + Sanctum |
| Внешние (мобильные, интеграции, партнёры) | Content Delivery, CRUD сущностей | API-токен (Sanctum) |
API — не обязательный слой для простого сайта-визитки (можно не активировать внешний доступ), но контракт заложен в ядре с фазы 0, чтобы острова и кабинет работали единообразно.
Два транспорта: REST (базовый) + GraphQL (опционально)
| REST | GraphQL | |
|---|---|---|
| Слой | ядро (всегда) | инфра-модуль (по потребности) |
| Инструмент | Laravel + API Resources | Lighthouse (SDL из типов) |
| Кому | острова, кабинет, простые интеграции | мобильные с гибкими выборками, партнёры |
| Формат | JSON:API-подобный (данные + meta + links) | схема + интроспекция |
REST — базовый и обязательный. GraphQL включается модулем, когда клиенту (обычно мобильному) нужны гибкие выборки — не навязывается каждому сайту.
Версионирование API
Версия в URL-префиксе — предсказуемо и кешируемо:
/api/v1/pages
/api/v1/content/{type}
/api/v2/… ← мажор при breaking change контракта APIПравила (согласованы с центром обновлений):
- аддитивные изменения (новое поле в ответе, новый эндпоинт) — в текущей версии, без bump;
- ломающие (удаление/переименование поля, смена типа, смена семантики) — новая мажорная версия
/api/v2/, старая живёт с deprecation-заголовком (Deprecation: true,Sunset: <дата>); - минимум одна предыдущая мажорная версия поддерживается параллельно (graceful-деградация старых мобильных клиентов — интеграции);
- версия API независима от версии ядра: рефакторинг ядра не меняет
/api/v1, пока контракт ответа тот же.
Формат ответа (единый)
Один конверт для всех эндпоинтов — предсказуемость для клиента и для Claude Code:
jsonc
// успех
{ "data": { … } | [ … ], "meta": { "next_cursor": "…", "has_more": true }, "links": {…} }
// ошибка (не раскрывать stack trace — APP_DEBUG=false инвариант)
{ "error": {
"type": "invalid_request", // класс: invalid_request | api_error | idempotency_error
"code": "validation_failed",
"message": "…",
"fields": { "email": ["…"] },
"doc_url": "https://…/errors#validation_failed" } }- трансформация модели → JSON только через API Resources, не отдаём Eloquent напрямую (утечка полей, N+1) — controllers spec;
Request-Id(ULID) в заголовке каждого ответа + тот же id в контексте логов — жалоба интегратора находится в логах за секунды (образец Stripereq_…);- пагинация/сортировка/фильтрация — через
spatie/laravel-query-builder(whitelisted поля, не произвольный SQL); - sparse fieldsets (
?fields=title,slug) — клиент берёт только нужное.
Пагинация и выборки
cursorPaginate— канон (реализовано с волны 9):meta.next_cursor+meta.has_more; offset-пагинация в/api/v1запрещена (см. ТЗ ядра); исключение «UI со страницами» относится только к server-rendered Blade-страницам вне API;meta.total— опционален:COUNT(*)выполняется только по явному?with_count=1(урок Bitrix: 2.4 млн строк — 49.9 с с count против 0.097 с без);- includes/relations — whitelist + лимит глубины ≤ 4 (Stripe expand; wildcard-развороты без потолка — 45-секундные запросы, боль Directus); связь по умолчанию — ID, объект — по явному include;
- операторы фильтров — конечный словарь на whitelist-полях; группы
_and/_orи кванторы_some/_noneдля hasMany — добавлять по мере потребности (модель Directus); - reconciliation-канал: на ресурсах для интеграций — фильтр
?updated_since=+ cursor. Вебхуки не гарантируют доставку (доктрина Shopify) — штатная сверка обязана быть дешёвой.
Идемпотентность и конкурентность мутаций
Обязательный слой до первой внешней интеграции (ретраи 1С/CRM без него создают дубли):
- заголовок
Idempotency-Key(UUID) на мутациях с денежным/внешним эффектом, включая выпуск токенов и ключей (скоуп уточнён ревизией 14.07.2026; на прочих POST — опционален): сервер кэширует статус+тело первого ответа на 24 ч; повтор → тот же ответ + заголовокIdempotent-Replayed: true; - тот же ключ + другое тело → fingerprint-сравнение → ошибка
idempotency_error; - конкурентный повтор ключа → 409
idempotency_key_in_use; - клиенту документируется: 4xx → новый ключ, 5xx/сеть → повтор с тем же;
- одновременные мутации одного объекта (1С и админка) —
Cache::lock("{model}:{id}"), таймаут → 429lock_timeout.
Batch
POST /api/v1/batch — массив независимых запросов (лимит ~25), флаг halt, весь пакет = 1 хит rate-лимитера. Зависимые цепочки ($result[...] Битрикса) сознательно не поддерживаются — url-кодированный ад, который Bitrix сам переписал в v3; для сложных выборок есть GraphQL-модуль и композитные BFF-эндпоинты. Массовые обмены (сотни тысяч записей) — не batch, а асинхронный bulk-канал JSONL (уроки §8, фаза 3).
Коды ответов
| Код | Когда |
|---|---|
| 200 / 201 / 204 | GET / POST-создал / DELETE без тела |
| 401 / 403 | не аутентифицирован / нет прав (Policy ∩ ability токена) |
| 404 | ресурс не найден |
| 409 | конкурентный повтор Idempotency-Key (idempotency_key_in_use) |
| 422 | ошибка валидации (FormRequest из движка полей); тот же ключ с другим телом (idempotency_error) |
| 429 | превышен rate-limit или таймаут лока объекта (lock_timeout) |
| 500 | серверная ошибка — без деталей в проде |
Дополнения ревизии 14.07.2026 (сводка в ТЗ ядра):
- деградация публичного чтения — недоступность взаимозаменяемого провайдера (поиск, подсказки, курсы) даёт
200+meta.degraded: true(+meta.degraded_reason), а не 5xx; 5xx — только отказ самого ядра; - persistent-роуты — комплаенс-роут модуля (One-Click Unsubscribe RFC 8058, отзыв согласия) может быть помечен
persistent: живёт при выключенном модуле, запросы складываются в очередь/журнал.
Аутентификация и права
- Sanctum — API-токены для внешних клиентов + сессия/SPA-режим для островов и кабинета (один механизм, два режима);
- token abilities обязательны (урок Bitrix-scopes): токен выдаётся со scope-списком (
{область}:{read|write}, напр.catalog:read,leads:write); итоговое право = ability ∩ Policy владельца — токен интеграции ≠ полный доступ пользователя; - права — та же RBAC (Shield + spatie/permission, Q11): API не имеет отдельной модели прав, использует Policy ядра — эндпоинт проверяет то же разрешение, что и админка; резолв прав кэшируется (боль Directus #25861 — до 2 минут на request без кэша);
- test/live-режим в префиксе токена (
cms_test_…/cms_live_…, урок Stripe) + флагlivemodeв событиях — стейджинг-интеграция физически не мутирует прод; GET /api/v1/permissions/me— интроспекция эффективных прав токена (none/partial/fullper ресурс + поля, образец Directus): острова и кабинет строят формы по этому ответу, не хардкодя роли;- модуль, добавляющий API-эндпоинты, декларирует их права в манифесте (контракт модуля) — «модуль ≠ ядро по правам» распространяется и на API.
Rate-limit и защита
throttleна каждый POST/PUT/DELETE и на внешние GET (не на совести клиента);- ключ лимитера — токен для аутентифицированных, IP — только для гостей (интеграции за одним NAT не должны делить лимит);
- заголовки
RateLimit-Limit/RateLimit-Remaining/RateLimit-Resetв ответах API; 429 — в едином конверте ошибки с указанием причины; - разные лимиты по аудитории: острова (мягко, по сессии), внешние токены (по токену/плану); лимит как атрибут тарифа клиента — заготовка под фазу 5 (модель Shopify);
- batch-пакет = 1 хит лимитера; bulk-операции — вне общего лимита (свой лимит «1 активная операция»);
- submission форм — rate-limit + honeypot + CSRF на уровне ядра (движок полей);
- CORS — whitelist origin'ов (кабинет, доверенные интеграции), не
*.
Генерация OpenAPI (docs-as-code)
Спека генерируется из кода, не пишется руками (DX):
контроллеры + API Resources + атрибуты ──Scramble/L5-Swagger──▶ openapi.json
движок полей (схемы сущностей) ──────────────────────▶ описания моделей
│
┌─────────────────────────┼──────────────┐
Scalar/Swagger UI SDK-клиенты Postman
(песочница) (автоген) коллекция- OpenAPI-атрибуты на публичных эндпоинтах — обязательны по контракту модуля;
- генерация в CI при каждом релизе → у каждой версии ядра/модуля своя актуальная спека;
- расхождение кода и доков невозможно (один источник);
- GraphQL — SDL из типов Lighthouse + GraphiQL-интроспекция.
Runtime-схема — обязательное дополнение (уроки §1): статическая генерация из кода не видит типы контента, созданные клиентом в рантайме. Поэтому:
GET /api/v1/schema— мёрж статического OpenAPI со схемами runtime-типов из FieldEngine (аналог/server/specs/oasDirectus иrest.documentation.openapiBitrix v3); созданный в админке тип виден в схеме без деплоя;GET /api/v1/content/{type}/fields— machine-readable схема полей конкретного типа (аналог{entity}.fieldsBitrix);- схема урезается под права токена (Directus
reduceSchema): клиент видит только свои коллекции и поля — OpenAPI не раскрывает чужие модули.
Поскольку сущности и поля описаны декларативно (движок полей, BlockRegistry), из тех же деклараций генерируются справочники блоков/событий/permissions/настроек — и для человека (панель «Разработчику»), и для Claude Code (машинный JSON).
Preview API (headless-превью черновиков)
Стык с версионированием страниц (Q7) и моделью данных:
- обычный API отдаёт только published-ревизию (как page-cache);
- предпросмотр черновика — отдельный эндпоинт по подписанному токену, в обход кеша, отдаёт последнюю (draft) ревизию;
- используется островами предпросмотра и внешним headless-фронтом (если клиент строит свой).
Островам и кабинету — тот же контракт
Важный инвариант: острова (зона 1) и кабинет (зона 3) ходят в тот же /api/v1, что и внешние клиенты, — не в отдельный «внутренний» API. Это гарантирует, что:
- один формат ответа, одна валидация, одни права везде;
- фильтр каталога на публичной странице и в кабинете — один эндпоинт;
- нет дублирования «внутренний контроллер для острова + внешний для API».
Разница только в аутентификации (сессия/CSRF для островов, токен для внешних) — контракт данных общий.
Контрактные тесты API (cms-testing)
- каждый эндпоинт отдаёт единый конверт (data/meta/links или error) +
Request-Id; - OpenAPI-спека генерируется без ошибок и покрывает все публичные эндпоинты; runtime-тип контента появляется в
GET /schemaбез деплоя; - права проверяются той же Policy, что в админке (нет обхода RBAC через API); токен
catalog:readне может мутировать; test-токен не видит live-данные; - rate-limit срабатывает (429) на POST-эндпоинтах; ключ лимитера — токен, не IP;
- ломающее изменение контракта требует новой
/api/vN(проверка обратной совместимости v1 в CI — условие «graceful-деградация старых клиентов»); - published-only в обычном API; draft — только по токену preview;
- повтор POST с тем же
Idempotency-Keyне создаёт дубль; тот же ключ с другим телом —idempotency_error; конкурентный повтор — 409; - list-запрос без
?with_count=1не выполняетCOUNT(*)(перф-инвариант); - схема из
GET /schemaдля токена без scope не содержит чужих коллекций.
Чеклист фазы 0 (API-контракт)
- [ ] REST в ядре + GraphQL как модуль; версионирование
/api/vN. - [ ] Единый конверт ответа (data/meta/links, error с type/code/doc_url) +
Request-Id. - [ ] API Resources (не сырой Eloquent) + query-builder (whitelisted фильтры, глубина ≤ 4).
- [ ] cursorPaginate +
has_more;totalтолько по?with_count=1;?updated_since=на интеграционных ресурсах (reconciliation). - [ ] Idempotency-Key middleware (кэш 24 ч, fingerprint, 409) + лок мутаций объекта.
- [ ]
POST /api/v1/batch— независимые запросы, 1 хит лимитера. - [ ] Sanctum (токен + SPA-режим); abilities-скоупы ∩ Policy; test/live-префикс токена;
GET /permissions/me. - [ ] Rate-limit на всех мутациях (ключ — токен) + RateLimit-заголовки + CORS whitelist + защита форм.
- [ ] OpenAPI из кода (Scramble) + runtime-схема
GET /schema(урезанная под права), генерация в CI, песочница Scalar. - [ ] Preview API черновиков по подписанному токену.
- [ ] Острова и кабинет — тот же
/api/v1, не отдельный внутренний API. - [ ] Контрактные тесты API (9 пунктов выше).
Связи
- Интеграции → API-слой — концепция и потребители.
- DX для Claude Code — docs-as-code.
- Канонический стек — версии, потребители по зонам.
- Движок полей — схемы сущностей → OpenAPI + валидация.
- Контракт модуля — модуль декларирует API-эндпоинты и права.
- controllers spec — правила API-контроллеров.
- specs/api.md — универсальные API-конвенции студии (все проекты).
- Разборы-источники: Bitrix24 REST, Directus/Stripe/Shopify.