На прошлой неделе я снова потратил полдня на то, чтобы понять, почему фронт падает после обновления бэка. Локально работало, а на стейдже ошибка. Оказалось, бэкендер переименовал поле в ответе, но не обновил документацию и не предупредил команду. Я узнал об этом только когда код упал на стейдже - вручную править ручку пришлось уже постфактум, разбираясь с ошибкой.

С этим надо было что-то делать.


Решение: генерируемый API-клиент

Я начал использовать генерируемый API-клиент. По сути, это набор ручек (функций для запросов) и типов к ним, которые генерируются на основе открытого API - yaml-файла сваггера.

Теперь на фронте я просто ввожу в терминал команду:

npm run openapi:pull

Она втягивает в проект актуальный yaml-файл, парсит его и выдает папку api/ в корне проекта. В api/generated лежат два главных файла: со всеми ручками и с их типами (описаниями структуры данных).


Как выглядит мой флоу обновления

Я скачиваю свежий spec (спецификацию - описание API) с бэкенда одной командой:

npm run openapi:pull

Под капотом она делает две вещи:

  1. Скачивает openapi.yaml с бэкенда через curl

  2. Запускает генерацию на основе скачанного файла

Иногда мне нужно только перегенерировать клиент из уже лежащего в репозитории spec - без скачивания с бэкенда:

npm run openapi:generate

В качестве генератора я использую @hey-api/openapi-ts.

Мой конфиг openapi-ts.config.ts выглядит так:

import { defineConfig } from '@hey-api/openapi-ts'
export default defineConfig({
input: 'api/openapi.yaml',
output: 'api/generated/',
clean: true,
client: '@hey-api/client-fetch'
})

Важный момент: я коммичу в репозиторий и api/openapi.yaml, и папку api/generated/. Но руками generated/ никогда не правлю - только через генерацию.


Как устроена структура api/

Папка api/ у меня организована так:

Файл/папка

Назначение

openapi.yaml

Источник правды - OpenAPI-спецификация, скачанная с бэкенда

generated/

Всё, что сгенерировано: sdk.gen.ts, types.gen.ts, client.gen.ts, core/

client.ts

Реэкспорт клиента из generated

sdk.ts

Реэкспорт функций эндпоинтов, например whoIsThere

types.ts

Реэкспорт всех типов

index.ts

Единая точка входа: клиент + все типы


Что я использую в приложении

Функции для запросов я импортирую прямо из ~/api/generated. Например, в сторах, страницах или блоках:

import { whoIsThere, getBasketOfShame } from '~/api/generated'

Типы я импортирую из ~/api (через index.ts):

import type { User, BasketOfShameResponse } from '~/api'

Обёртку над запросами (baseURL, заголовки, обработка ошибок) я настраиваю отдельно, это не относится к генерации, поэтому в статье не рассматриваю.


Что делать, если бэкенда ещё нет?

Я храню моки (заглушки для тестирования) в папке mock/. Каждый мок - это обычный json-файл, соответствующий структуре ответа. Когда бэкенд готов, я просто удаляю моки и подключаю реальный клиент.


Как это повлияло на мою разработку

В коде и шаблонах теперь всегда используются только актуальные поля, приходящие с бэка. Мне не нужно держать в голове корректность именований - TypeScript сам подсказывает на этапе написания кода. Если я опечатался в имени поля или передал не тот тип, IDE сразу подсвечивает ошибку.

Исчезла головная боль с расхождениями в контракте (договорённости о том, как именно клиент и сервер обмениваются данными). Раньше огромное количество времени улетало на то, чтобы понять, кто виноват: фронтенд неправильно стучится или бэкенд неправильно отвечает. Теперь я всегда знаю, где лежат ручки, для чего нужна каждая из них, как ими пользоваться и какие типы данных участвуют в контракте. Всё это прямо в коде проекта - даже сваггер открывать не нужно.

Любой новый фронтендер, приходящий на проект и прочитавший короткое ридми, сразу знает, куда смотреть. На небольших проектах, где задача - вывести данные или отправить их назад, это особенно удобно: я практически полностью избавился от ручной API-типизации, которая требует постоянной актуализации, и от ручного написания каждой новой ручки.


И да, сместился фокус ответственности

Мы шутим, что теперь, если что-то сломалось, виноват бэкенд. Но на самом деле ответственность за работоспособность API действительно смещается на бэк.

С ручной реализацией, если запрос не работал, первопричину искали долго: либо фронтенд неверно запрашивает, либо бэкенд неверно отвечает.

Теперь всё проще. Если запрос не работает, есть два места, куда смотрим в первую очередь:

  • либо сваггер некорректно заполнен и не соответствует реальному контракту, который ожидает бэк;

  • либо - что чаще - просто бэковая поломка.


Но я не перекладываю работу

Моя цель - единая точка входа в контракт клиент-серверного взаимодействия. Флоу починки бага теперь максимально линеен:

  1. Бэк чинит запрос

  2. Бэк обновляет сваггер

  3. Я одной командой втягиваю обновленный yaml-файл и обновляю код по актуальному контракту


Подводные камни, с которыми я столкнулся

Всё было бы идеально, если бы не пара моментов.

oneOf / anyOf. Генератор не всегда красиво обрабатывает сложные схемы с пересечениями. Иногда приходится вручную дописывать гарды (проверки типов), чтобы TypeScript понял, что именно пришло.

Сваггер не совпадает с реальностью. Бывает, что в openapi.yaml написано одно, а бэк возвращает другое. В этом случае TypeScript не просто бесполезен - он активно мешает. Типы говорят, что пришло одно, а по факту приходит другое. Единственный способ успокоить компилятор - писать кучу проверок, гардов и as-кастов, чтобы код просто не краснел. А если поле вообще отсутствует, то вместо понятной ошибки ты получаешь undefined в рантайме, хотя TypeScript утверждает, что поле обязательное. В итоге ты тратишь время не на поиск реальной проблемы, а на то, чтобы TypeScript просто успокоился.

Трансформация данных на фронте. Сгенерированные DTO (объекты передачи данных) - это не доменные модели (внутренние объекты приложения с бизнес-логикой). Иногда я оборачиваю их в адаптеры (прослойки, преобразующие данные), чтобы добавить вычисляемые поля или переименовать для удобства внутри приложения.

Лучше коммитить spec. Я храню openapi.yaml в репозитории, чтобы сборка не падала, если бэкенд внезапно недоступен.


Что в итоге

Это работает. Я попробовал и возвращаться к ручному написанию ручек уже не хочу. Генерация API-клиента экономит время, убирает головную боль с типизацией и делает процесс разработки предсказуемым.

Если вы ещё не пробовали - начните с малого. Один небольшой проект. Мне хватило, чтобы понять, что возвращаться к ручному написанию ручек я уже не буду.

Комментарии (5)


  1. DmitryKazakov8
    29.06.2026 14:07

    Добро пожаловать на Хабр.

    Идея генерировать апи-клиент с типизацией из openapi стара, и ее недостатки достаточно изучены, чтобы сказать - это скорее нишевая схема, в большинстве проектов добавляет больше неразберихи и проблем, чем пользы.

    Часть проблем вы уже нашли сами, с другой частью еще столкнетесь:

    • схема в реальности может расходиться с фактически присланными данными (по моему опыту - частая история), то есть гарантии мы не получаем

    • проблемы с версионированием - если скажем 10 разработчиков бэк+фронт работают над разными ручками в рамках своих задач, нужна сложная инфраструктура чтобы удобно разрабатывать. Это отдельное пространство для проблем, которые можно "героически решать", например публикуя с yaml или с готовым апи-клиентом пакеты во внутренние репозитории, генерируя в день сотни "@api-spec": "123.3.1877-feature-123", синхронизируя со стендами и деплойными циклами, занимаясь сложными слияниями в dev. Ну либо перекидываться в рабочих чатах yaml файлами, а потом на dev стенде после слияния нескольких фич ловить что или фронт или бэк вмерджили что-то не выложенное на стенд или ушли в глубокий конфликт.

    • фронту может быть нужен 1 параметр в конкретной ручке, а бэк присылает 100 (возможно - легаси, или для разных приложений-потребителей). Сгенерированный апи-клиент не очистит лишнее, ему все кажется важным и нужным.

    • в ряде кейсов теряется параллельность разработки бэка и фронта. В задаче согласовали переименование параметра в ручке - фронт не может у себя поменять, проверить, доработать типы и связанный код. Он должен дождаться пока бэк либо сделает задачу, либо сделает заглушки -> генерируется openapi -> генерируется апи-клиент -> можно начинать работу. Либо костылить и реплейсить вручную части сгенерированного апи-клиента.

    • большинство cli-утилит генерации заточены под единый огромный инстанс апи-клиента. Сложно будет сделать lazy loading, разбиение на чанки, модульность (чтобы апи лежало ближе к местам использования), не потеряв единый процесс фетчинга и не наплодив дубляж.

    • велик риск что в апи-клиент попадут неиспользуемые ручки, и в целом анализ по проекту "какие именно ручки и какие именно поля фронт использует" будет затруднен. Бандл неизбежно раздувается, на Хабр летят статьи "почему вкладки браузера тормозят и едят столько памяти".

    • логирование несовпавших ожиданий все равно требуется - то есть необходимо где-то хранить "что именно нужно фронту для работы", и делать дифф со сгенерированным апи-клиентом.

    Ну, о недостатках можно говорить очень долго, суть - "сгенерированный апи-клиент по openapi может гарантировать, что он соответствует openapi, но абсолютно не гарантирует что он соответствует конкретному фронту-потребителю".

    Конечно, есть кейсы, где это все подходит на каком-то из этапов развития проекта (особенно если количество фронтендеров === 1 и проект маленький), но он скорее тупиковый.

    Как не сталкиваться с этими проблемами? Воткнуть генерацию в правильное место - не в то, чтобы создавать js-файл "что примерно отдает бэк", а в то, чтобы "производилась валидация ожиданий между тем, что нужно фронту и что реально отдает бэк". Написать руками DTO для ручек только с теми полями, которые нужны конкретному фронту-потребителю, а дальше уже подключать генерацию, как примерно описывал тут.


    1. grmnche Автор
      29.06.2026 14:07

      Рад быть тут :) Да, сложно не согласиться, что такой подход имеет как свои преимущества, некоторые из которых я описал, так и недостатки в виде сложности с масштабированием и потребности в четком версионировании. Для мелких проектов - я прямо-таки доволен. Спасибо за такой развернутый комментарий о подводных камнях, это ценно


      1. DmitryKazakov8
        29.06.2026 14:07

        Главное - успейте вовремя остановиться на текущем пути) Если поверх сгенерированного по спеке апи-клиента еще прикручивать автоматические валидаторы типа Zod, то одна из проблем формально решится - то, что бэк реально отдает, начнет валидироваться. Но добавятся новые - фронт будет логировать ошибки или падать при изменении неиспользуемых полей. По факту фронт становится "валидатором, что бэк правильно сгенерировал спеку" - это вообще путь не туда. Я потратил пару лет на этот путь, и продвинулся довольно далеко в энтерпрайзах - но проблемы будут множиться, инфраструктура становиться все сложнее, а по времени и ресурсам бизнесу это все куда дороже, чем даже вручную править.

        потратил полдня на то, чтобы понять, почему фронт падает после обновления бэка

        Решение этого - не в проверке соответствия бэкенда им же написанной спеке. Просто предостерегаю от излишнего погружения в такую схему - лучше не делать дефолтной для проектов, есть подходы эффективнее.


        1. grmnche Автор
          29.06.2026 14:07

          Спасибо! Это отличный аргумент из практики) На что вы перешли в итоге после того, как пришли к тому, что генерируемый клиент - не тот путь, который вам нужен?


          1. DmitryKazakov8
            29.06.2026 14:07

            Не понимаю вопрос - в конце первого комментария я описал схему работы и дал ссылку на пример кода.