Skip to content

Фаза 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 (опционально)

RESTGraphQL
Слойядро (всегда)инфра-модуль (по потребности)
ИнструментLaravel + API ResourcesLighthouse (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 в контексте логов — жалоба интегратора находится в логах за секунды (образец Stripe req_…);
  • пагинация/сортировка/фильтрация — через 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}"), таймаут → 429 lock_timeout.

Batch

POST /api/v1/batch — массив независимых запросов (лимит ~25), флаг halt, весь пакет = 1 хит rate-лимитера. Зависимые цепочки ($result[...] Битрикса) сознательно не поддерживаются — url-кодированный ад, который Bitrix сам переписал в v3; для сложных выборок есть GraphQL-модуль и композитные BFF-эндпоинты. Массовые обмены (сотни тысяч записей) — не batch, а асинхронный bulk-канал JSONL (уроки §8, фаза 3).

Коды ответов

КодКогда
200 / 201 / 204GET / 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/full per ресурс + поля, образец 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/oas Directus и rest.documentation.openapi Bitrix v3); созданный в админке тип виден в схеме без деплоя;
  • GET /api/v1/content/{type}/fields — machine-readable схема полей конкретного типа (аналог {entity}.fields Bitrix);
  • схема урезается под права токена (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)

  1. каждый эндпоинт отдаёт единый конверт (data/meta/links или error) + Request-Id;
  2. OpenAPI-спека генерируется без ошибок и покрывает все публичные эндпоинты; runtime-тип контента появляется в GET /schema без деплоя;
  3. права проверяются той же Policy, что в админке (нет обхода RBAC через API); токен catalog:read не может мутировать; test-токен не видит live-данные;
  4. rate-limit срабатывает (429) на POST-эндпоинтах; ключ лимитера — токен, не IP;
  5. ломающее изменение контракта требует новой /api/vN (проверка обратной совместимости v1 в CI — условие «graceful-деградация старых клиентов»);
  6. published-only в обычном API; draft — только по токену preview;
  7. повтор POST с тем же Idempotency-Key не создаёт дубль; тот же ключ с другим телом — idempotency_error; конкурентный повтор — 409;
  8. list-запрос без ?with_count=1 не выполняет COUNT(*) (перф-инвариант);
  9. схема из 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 пунктов выше).

Связи

Внутренняя база знаний студии