Контекст
У каждого вызова в этом репозитории два конца: обработчик на Go и вызывающий код на TypeScript в том же монорепозитории. Поддерживать типы обмена вручную с обеих сторон — значит сделать расхождение невидимым до момента, когда его увидит клиент. И монорепозиторий делает расхождение более вероятным, а не менее: участник, меняющий обработчик, может поменять его вызов в том же коммите и нигде это не зафиксировать. Второе давление — API публичный. У оператора, который пишет скрипт к/v1/admin, нет доступа к обработчику, поэтому то, что сказано в контракте, и
есть то, под что он пишет код.
Решение
api/openapi.yaml — исходный контракт. Типы сервера на Go генерируются из него
через oapi-codegen, а привязки Fetch/SWR на TypeScript и схемы Zod — через
Orval. Ни одна сторона не поддерживает дублирующий тип обмена вручную, и CI
падает, если изменение контракта закоммичено без перегенерации обеих сторон.
Контракт — OpenAPI 3.0.3, а не 3.1, потому что у выбранного генератора для Go
нет стабильной поддержки 3.1. Это ограничение инструментария, записанное здесь,
чтобы следующий человек не потратил на его обнаружение полдня.
Существуют три префикса, и они остаются раздельными: /v1/panel (cookie сессии
оператора, CSRF, RBAC), /v1/admin (bearer-токен, для скриптов и операторского
инструментария в Telegram) и /v1/account (cookie сессии клиента, CSRF, без
RBAC). Их не объединяют: при объединении каждый унаследовал бы промежуточный слой
другого, а учётные данные клиента не должны дотягиваться до операторского
маршрута.
Соглашения, общие для всех трёх: ответы об ошибках по RFC 9457, курсорная
пагинация, метки времени в UTC, деньги целыми числами в минорных единицах,
идентификаторы запросов и ключи идемпотентности на изменяющих операциях.
Последствия
Генерация — шаг сборки с зубами:bun run generate:api и следом
git diff --exit-code в CI означают, что неперегенерированное изменение
контракта не пройдёт.
Сгенерированный код заканчивается в двух местах, и оба названы здесь, а не
обнаруживаются потом:
- Сгенерированные типы сервера на Go никем не импортируются. Обработчики декодируют в анонимные структуры и пишут собранные вручную отображения, поэтому ответ может разойтись со своей схемой без падения теста. Контракт авторитетен для вызывающих, но не проверяется в рантайме по ответам.
- Панели вызывают общий типизированный транспорт, а не сгенерированные хуки
SWR. Генератор Orval с
client: "swr"выпускает голые вызовыfetchи игнорируетoverride.mutator, поэтому сгенерированный хук не нёс бы ни cookie сессии, ни токен CSRF. Обе стороны нацелены на один и тот же сгенерированный контракт; различается только транспорт.
/v1/panel, и ни одна операция
панели или личного кабинета не ссылается на свою схему безопасности. Эти пробелы
перечислены в Интеграции с API, чтобы тот, кто генерирует
клиент, встретил их раньше, чем это сделает его код.
Когда пересматривать
- У генератора для Go появляется стабильная поддержка OpenAPI 3.1, и контракт может перейти на неё, получив возможности JSON Schema, которых нет в 3.0.3.
- Orval начинает поддерживать mutator для клиента SWR, и тогда панели могут использовать сгенерированные хуки, а общий транспорт становится лишним.
- Проверка ответов по контракту оправдывает свою цену — скорее всего как проверка во время тестов, прогоняющая обработчики через схему, а не как промежуточный слой на пути запроса.
Смежное
- Интеграция с API — публичная поверхность, её правила и её пробелы.
- Технологический стек — сами генераторы.
- Политика расширения — какие части этого контракта стабильны для использования.