Skip to main content
Статус: принято.

Контекст

У каждого вызова в этом репозитории два конца: обработчик на 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, и тогда панели могут использовать сгенерированные хуки, а общий транспорт становится лишним.
  • Проверка ответов по контракту оправдывает свою цену — скорее всего как проверка во время тестов, прогоняющая обработчики через схему, а не как промежуточный слой на пути запроса.

Смежное