Оптимизация без AI: как я автоматизировал API-ручки и типы

от автора

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

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


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

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

ссылка на оригинал статьи https://habr.com/ru/articles/1053396/