На прошлой неделе я снова потратил полдня на то, чтобы понять, почему фронт падает после обновления бэка. Локально работало, а на стейдже ошибка. Оказалось, бэкендер переименовал поле в ответе, но не обновил документацию и не предупредил команду. Я узнал об этом только когда код упал на стейдже — вручную править ручку пришлось уже постфактум, разбираясь с ошибкой.
С этим надо было что-то делать.
Решение: генерируемый API-клиент
Я начал использовать генерируемый API-клиент. По сути, это набор ручек (функций для запросов) и типов к ним, которые генерируются на основе открытого API — yaml-файла сваггера.
Теперь на фронте я просто ввожу в терминал команду:
npm run openapi:pull
Она втягивает в проект актуальный yaml-файл, парсит его и выдает папку api/ в корне проекта. В api/generated лежат два главных файла: со всеми ручками и с их типами (описаниями структуры данных).
Как выглядит мой флоу обновления
Я скачиваю свежий spec (спецификацию — описание API) с бэкенда одной командой:
npm run openapi:pull
Под капотом она делает две вещи:
-
Скачивает
openapi.yamlс бэкенда черезcurl -
Запускает генерацию на основе скачанного файла
Иногда мне нужно только перегенерировать клиент из уже лежащего в репозитории 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-спецификация, скачанная с бэкенда |
|
|
Всё, что сгенерировано: |
|
|
Реэкспорт клиента из |
|
|
Реэкспорт функций эндпоинтов, например |
|
|
Реэкспорт всех типов |
|
|
Единая точка входа: клиент + все типы |
Что я использую в приложении
Функции для запросов я импортирую прямо из ~/api/generated. Например, в сторах, страницах или блоках:
import { whoIsThere, getBasketOfShame } from '~/api/generated'
Типы я импортирую из ~/api (через index.ts):
import type { User, BasketOfShameResponse } from '~/api'
Обёртку над запросами (baseURL, заголовки, обработка ошибок) я настраиваю отдельно, это не относится к генерации, поэтому в статье не рассматриваю.
Что делать, если бэкенда ещё нет?
Я храню моки (заглушки для тестирования) в папке mock/. Каждый мок — это обычный json-файл, соответствующий структуре ответа. Когда бэкенд готов, я просто удаляю моки и подключаю реальный клиент.
Как это повлияло на мою разработку
В коде и шаблонах теперь всегда используются только актуальные поля, приходящие с бэка. Мне не нужно держать в голове корректность именований — TypeScript сам подсказывает на этапе написания кода. Если я опечатался в имени поля или передал не тот тип, IDE сразу подсвечивает ошибку.
Исчезла головная боль с расхождениями в контракте (договорённости о том, как именно клиент и сервер обмениваются данными). Раньше огромное количество времени улетало на то, чтобы понять, кто виноват: фронтенд неправильно стучится или бэкенд неправильно отвечает. Теперь я всегда знаю, где лежат ручки, для чего нужна каждая из них, как ими пользоваться и какие типы данных участвуют в контракте. Всё это прямо в коде проекта — даже сваггер открывать не нужно.
Любой новый фронтендер, приходящий на проект и прочитавший короткое ридми, сразу знает, куда смотреть. На небольших проектах, где задача — вывести данные или отправить их назад, это особенно удобно: я практически полностью избавился от ручной API-типизации, которая требует постоянной актуализации, и от ручного написания каждой новой ручки.
И да, сместился фокус ответственности
Мы шутим, что теперь, если что-то сломалось, виноват бэкенд. Но на самом деле ответственность за работоспособность API действительно смещается на бэк.
С ручной реализацией, если запрос не работал, первопричину искали долго: либо фронтенд неверно запрашивает, либо бэкенд неверно отвечает.
Теперь всё проще. Если запрос не работает, есть два места, куда смотрим в первую очередь:
-
либо сваггер некорректно заполнен и не соответствует реальному контракту, который ожидает бэк;
-
либо — что чаще — просто бэковая поломка.
Но я не перекладываю работу
Моя цель — единая точка входа в контракт клиент-серверного взаимодействия. Флоу починки бага теперь максимально линеен:
-
Бэк чинит запрос
-
Бэк обновляет сваггер
-
Я одной командой втягиваю обновленный
yaml-файл и обновляю код по актуальному контракту
Подводные камни, с которыми я столкнулся
Всё было бы идеально, если бы не пара моментов.
oneOf / anyOf. Генератор не всегда красиво обрабатывает сложные схемы с пересечениями. Иногда приходится вручную дописывать гарды (проверки типов), чтобы TypeScript понял, что именно пришло.
Сваггер не совпадает с реальностью. Бывает, что в openapi.yaml написано одно, а бэк возвращает другое. В этом случае TypeScript не просто бесполезен — он активно мешает. Типы говорят, что пришло одно, а по факту приходит другое. Единственный способ успокоить компилятор — писать кучу проверок, гардов и as-кастов, чтобы код просто не краснел. А если поле вообще отсутствует, то вместо понятной ошибки ты получаешь undefined в рантайме, хотя TypeScript утверждает, что поле обязательное. В итоге ты тратишь время не на поиск реальной проблемы, а на то, чтобы TypeScript просто успокоился.
Трансформация данных на фронте. Сгенерированные DTO (объекты передачи данных) — это не доменные модели (внутренние объекты приложения с бизнес-логикой). Иногда я оборачиваю их в адаптеры (прослойки, преобразующие данные), чтобы добавить вычисляемые поля или переименовать для удобства внутри приложения.
Лучше коммитить spec. Я храню openapi.yaml в репозитории, чтобы сборка не падала, если бэкенд внезапно недоступен.
Что в итоге
Это работает. Я попробовал и возвращаться к ручному написанию ручек уже не хочу. Генерация API-клиента экономит время, убирает головную боль с типизацией и делает процесс разработки предсказуемым.
Если вы ещё не пробовали — начните с малого. Один небольшой проект. Мне хватило, чтобы понять, что возвращаться к ручному написанию ручек я уже не буду.
ссылка на оригинал статьи https://habr.com/ru/articles/1053396/