
Привет! Меня зовут Данил, я Frontend разработчик, живу в Питере, работаю в компании Unistory. Решил рассказать на Хабре, как автоматизировать один нудный процесс.
Проблематика
Зачастую нам приходится описывать запросы или переписывать уже имеющиеся ввиду изменения каких-то DTO (Data Transfer Object) или параметров у запросов. Это вполне естественно для разработки, но часто оказывается скучным и однотипным процессом, не требующим размышлений или особых навыков. Как следствие, его автоматизация кажется отличным выходом.
Решение
Автоматизация генерации API-контроллеров на клиенте возможна с помощью инструментов openapi-merge-cli и swagger-typescript-api. Они позволяют генерировать код на основе OpenAPI (Swagger) спецификации, минимизируя ручной труд и снижая вероятность ошибок.
Когда это уместно?
Данное решение отлично подходит для быстрого старта в проектах уровня MVP (Minimum Viable Product) или PoC (Proof of Concept). Однако, при разработке масштабируемого продукта с гибкой архитектурой оно может накладывать определенные ограничения, и в таких случаях стоит рассмотреть более детальную кастомизацию API-клиента.
Начало работы
Установка зависимостей:
npm i --save-dev openapi-merge-cli swagger-typescript-api
Добавление скриптов:
"merge-openapi": "npx openapi-merge-cli --config ./swagger/openapi.config.json", "codegen:api": "node codegen/codegen.js", "codegen": "npm run merge-openapi && npm run codegen:api",
Структура проекта:
project-root ├──
api # Папка для сгенерированных файлов (создается вручную пустой!) │ ├── Users.ts # Контроллер для работы с пользователями │ ├── Orders.ts # Контроллер для работы с заказами │ ├── http-client.ts # HTTP-клиент (обертка над axios/fetch) │ ├── data-contracts.ts # Типы и DTO, сгенерированные из спецификации │ ├── openapi.json # Объединённая OpenAPI спецификация после merge │ ├──
codegen # Папка с кодогенерацией API-клиента │ ├── codegen.js # Скрипт генерации API-клиента на основе OpenAPI спецификации │ ├──
swagger # Папка с OpenAPI спецификациями │ ├── openapi.config.json # Конфигурация объединения OpenAPI схем │ ├── api.swagger.json # OpenAPI спецификация бэкенд-сервиса (может быть несколько) │ ├── users.swagger.json # Спецификация для сервиса пользователей (если API разбито на части) │ ├── orders.swagger.json # Спецификация для сервиса заказов (если API разбито на части) │ ├──
src # Папка с кодом проекта │ ├── package.json # Конфигурация проекта, включая npm-скрипты для генерации API
Разберем подробнее пакеты и скрипты
openapi-merge-cli
Что делает?
openapi-merge-cli объединяет несколько OpenAPI-спецификаций в один файл.
Зачем?
Если у тебя API разбито на несколько частей (**-**например, users.json, orders.json), этот шаг создает единый openapi.json, который потом передаётся в генератор.
Как это работает в коде?
Чтобы объединить несколько OpenAPI-файлов в один, используется конфигурационный файл swagger/openapi.config.json. Он указывает, какие файлы должны быть объединены и куда сохранить итоговый результат.
Пример openapi.config.json:
// /swagger/openapi.config.json { "inputs": [ { "inputFile": "./users.swagger.json" }, { "inputFile": "./orders.swagger.json" } ], "output": "../api/openapi.json" }
Разбор конфигурации:
-
inputs – массив с путями к OpenAPI-файлам, которые нужно объединить. В этом примере соединяются users.swagger.json и orders.swagger.json.
-
output – путь, куда будет сохранён объединённый файл openapi.json.
swagger-typescript-api
Что делает?
swagger-typescript-api — это инструмент для генерации API-клиента и типов на основе OpenAPI-спецификации. Он анализирует openapi.json и создаёт готовый TypeScript-код для работы с API.
Основные возможности:
-
Генерация полностью типизированных API-запросов.
-
Автоматическое создание моделей (DTO) на основе OpenAPI-спеки.
-
Поддержка разных подходов: fetch, axios и других.
-
Гибкая конфигурация (например, можно менять структуру путей, типы, включать/выключать комментарии и т. д.).
Зачем?
-
Упрощает работу с API — тебе не нужно вручную писать интерфейсы и запросы.
-
Снижает вероятность ошибок, так как весь API-клиент типизирован.
-
Позволяет легко поддерживать актуальность API-кода при изменении бэкенда.
Как это работает в коде?
Файл codegen/codegen.js (который вызывается командой npm run codegen:api) нужно создать вручную, если его нет в проекте. В этом файле содержится конфигурация для кодгена swagger-typescript-api
// /codegen/codegen.js import path from 'path' import { generateApi } from 'swagger-typescript-api' // Генерация API-клиента на основе OpenAPI-спецификации generateApi({ // Имя выходного файла (если modular: false) name: 'api.ts', // Использовать единый HTTP-клиент (axios) singleHttpClient: true, // Выбор HTTP-клиента (axios или fetch) httpClientType: 'axios', // Разбить API-клиент на модули (файлы) modular: true, // Выносить тело запроса в отдельный параметр extractRequestBody: true, // Выносить query и path параметры в отдельный параметр extractRequestParams: true, // Выносить тело ответа в отдельный параметр extractResponseBody: true, // Выносить ошибки API в отдельный параметр extractResponseError: true, // Генерировать типы ответов API generateResponses: true, // Использовать объединённые enum-типы (`enum | string`) generateUnionEnums: true, // Исправлять ошибки OpenAPI-спеки (если есть несовместимости) patch: true, // Генерировать клиент для работы с API generateClient: true, // Индекс для именования модулей API moduleNameIndex: 1, // Путь к OpenAPI-файлу input: path.resolve(process.cwd(), './api/openapi.json'), // Папка для сохранения сгенерированного кода output: path.resolve(process.cwd(), './api'), })
Этот скрипт выполняет генерацию API-клиента на основе OpenAPI-спецификации. Он берет openapi.json, создаёт типизированные запросы и модели данных, а затем сохраняет их в ./api.
Общий процесс генерации API
-
Получаем API спецификацию → На странице сваггера находим OpenAPI JSON-схему. Как правило, она находится под названием проекта.
Снимок экрана 2025-03-10 в 14.52.44.png Если схемы нет, бекендер должен её описать в коде, используя инструменты типа NestJS Swagger, FastAPI, SpringDoc или другие подходящие для его стека. Без этого генерация API-клиента невозможна.
Также в данной реализации важно понимать, что при обновлениях бекенда, необходимо каждый раз обновлять спеку на клиенте и выполнять команду npm run codegen, но если статья вам покажется интересной, я обязательно напишу продолжение, в котором реализую автоматическое подтягивание спеки.
-
Выгружаем JSON схему на клиент → Полученную спеку нужно сохранить в файл swagger/api.swagger.json , если таковых несколько, разбиваем их по отдельным файлам и указываем пути в swagger/openapi.config.json , как правило схема отражает сервис с которым вы работаете.
Пример с одним бекенд сервисом:
// /swagger/openapi.config.json { "inputs": [ { "inputFile": "./api.swagger.json" } ], "output": "../api/openapi.json" }
Пример с отдельными сервисами:
// /swagger/openapi.config.json { "inputs": [ { "inputFile": "./identity.swagger.json" }, { "inputFile": "./payment.swagger.json" }, { "inputFile": "./users.swagger.json" }, { "inputFile": "./admin.swagger.json" } ], "output": "../api/openapi.json" }
-
Запускаем скрипт слияния и генерации → Для удобства я создал скрипт, который объединяет две команды в одну: npm run codegen. В результате выполнения мы получаем папку api в корне проекта с автоматически сгенерированными типами и контроллерами для работы с API. Внутри папки будут следующие файлы:
-
http-client.ts — сгенерированный HTTP-клиент, обёртка над axios, содержащая базовую логику запросов, работу с заголовками, авторизацией и обработку параметров.
-
data-contracts.ts — автоматически сгенерированные TypeScript-типы, соответствующие моделям из OpenAPI. Используются для типизации данных в проекте.
-
openapi.json — объединённый файл спецификации OpenAPI, описывающий все доступные API-эндпоинты.
-
Auth.ts, Users.ts и другие контроллеры — автоматически сгенерированные файлы с методами для работы с API. Они формируются на основе эндпоинтов, описанных в Swagger (OpenAPI), и содержат готовые функции для выполнения запросов. Например, Auth.ts включает методы для авторизации (login, logout), а Users.ts — для работы с пользователями (getUser, updateUser).
Чтобы использовать контроллеры, необходимо создать экземпляр HttpClient и передать его в контроллеры, обеспечивая единую точку конфигурации для запросов в проекте.
-
-
Объявляем HTTP Client → Создаем экземпляр HttpClient из сгенерированного кода, указывая базовый URL и необходимые параметры — фактически это аналог инстанса axios для работы с API. Файл с клиентом (http-client.ts) может располагаться в разных местах в зависимости от архитектурной методологии проекта (например, в папке services, lib или api). В моем случае это FSD (Feature-Sliced Design), где такой файл обычно находится в папке shared/api или shared/lib
// /src/shared/api/http-client.ts import { HttpClient } from '@api/http-client' import { Constants } from '@shared/lib' export const httpClient = new HttpClient({ baseURL: Constants.API_URL, // Другие параметры по необходимости })
-
Объявляем API-контроллеры → Создаём экземпляры сгенерированных контроллеров. В зависимости от архитектурного подхода их можно инициализировать прямо в http-client.ts для простого варианта или разделить код на модули, объявляя контроллеры внутри соответствующих модулей. Такой подход позволяет лучше структурировать код и упрощает масштабирование проекта.
-
Пример базовый:
// /src/shared/api/http-client.ts import { Auth } from '@api/Auth' import { Invoices } from '@api/Invoices' import { OAuthAccount } from '@api/OAuthAccount' import { Payments } from '@api/Payments' import { Subscriptions } from '@api/Subscriptions' import { Users } from '@api/Users' import { UserSubscriptions } from '@api/UserSubscriptions' import { HttpClient } from '@api/http-client' import { Constants } from '@shared/lib' export const httpClient = new HttpClient({ baseURL: Constants.API_URL, // Другие параметры по необходимости }) // Auth controllers export const authController = new Auth(httpClient) export const authIntegrationsController = new OAuthAccount(httpClient) // Subscriptions controllers export const invoicesController = new Invoices(httpClient) export const paymentsController = new Payments(httpClient) export const subscriptionsController = new Subscriptions(httpClient) export const userSubscriptionsController = new UserSubscriptions(httpClient) // Users controllers export const usersController = new Users(httpClient)
-
Пример с сущностями / модулями:
// /src/entites/user/api/users-controller.ts import { httpClient } from '@shared/api' import { Users } from '@api/Users' export const usersController = new Users(httpClient)
// /src/entites/auth/api/auth-controller.ts import { httpClient } from '@shared/api' import { OAuthAccount } from '@api/OAuthAccount' import { Auth } from '@api/Auth' export const authController = new Auth(httpClient) export const authIntegrationsController = new OAuthAccount(httpClient)
-
Что мы имеем?
Представим, что наш сваггер выглядел так:

После всех вышеперечисленных действий в папке api мы получим такую структуру:
project-root ├──
api # Папка для сгенерированных файлов (создается вручную пустой!) │ ├── Admins.ts # Контроллер для работы с администраторами │ ├── Auth.ts # Контроллер для работы с аутентификацией │ ├── CounterNftLotOffers.ts # Контроллер для управления ставками на NFT-лоты │ ├── Lots.ts # Контроллер для работы с лотами │ ├── Nft.ts # Контроллер для работы с NFT │ ├── NftLots.ts # Контроллер для работы с коллекциями NFT │ ├── Notifications.ts # Контроллер для работы с уведомлениями │ ├── PrivateOrders.ts # Контроллер для частных заказов │ ├── ScDeals.ts # Контроллер для сделок │ ├── ScDealsCharts.ts # Контроллер для графиков по сделкам │ ├── SeasonRewardHistories.ts # Контроллер для истории сезонных наград │ ├── SeasonUsers.ts # Контроллер для сезонных пользователей │ ├── Seasons.ts # Контроллер для сезонов │ ├── Users.ts # Контроллер для пользователей │ ├── http-client.ts # HTTP-клиент (обертка над fetch/axios) │ ├── data-contracts.ts # Типы и DTO, сгенерированные из OpenAPI спецификации │ ├── openapi.json # Объединённая OpenAPI спецификация после merge │ ├──
codegen # Папка с кодогенерацией API-клиента │ ├── codegen.ts # Скрипт генерации API-клиента на основе OpenAPI спецификации │ ├──
swagger # Папка с OpenAPI спецификациями │ ├── openapi.config.json # Конфигурация объединения OpenAPI схем │ ├── nft-market.swagger.json # OpenAPI спецификация NFT-маркета │ ├──
src # Папка с кодом проекта │ ├── package.json # Конфигурация проекта, включая npm-скрипты для генерации API ├── tsconfig.json # Конфигурация TypeScript (если используется TS)
Описание файлов:
-
Admins.ts, Auth.ts, CounterNftLotOffers.ts, …, Users.ts
-
Каждый файл представляет API-контроллер для соответствующей сущности.
-
Содержит классы и методы для запросов (GET, POST, PUT, DELETE).
-
Использует HTTP-клиент для выполнения запросов.
-
-
data-contracts.ts
-
Описывает типы данных (интерфейсы) для запросов и ответов API.
-
Используется во всех контроллерах.
-
-
http-client.ts
-
Отвечает за отправку HTTP-запросов.
-
Может содержать кастомную логику для работы с токенами и авторизацией.
-
-
openapi.json
-
Исходный OpenAPI (Swagger) JSON, на основе которого была сгенерирована API-структура.
-
Важно!
Не изменяйте вручную файлы контроллеров и типов — после обновления API и повторной кодогенерации ваши правки будут перезаписаны.
Если необходимо изменить типы, делайте это на уровне вашего приложения, расширяя или объединяя их, вместо редактирования сгенерированных файлов.
Как работать с полученными контроллерами и типами?
Если вы выполнили все описанные выше шаги, у вас уже есть готовые экземпляры API-контроллеров.
// /src/shared/api/http-client.ts import { Auth } from '@api/Auth' import { HttpClient } from '@api/http-client' import { Constants } from '@shared/lib' export const httpClient = new HttpClient({ baseURL: Constants.API_URL, // Другие параметры по необходимости }) // Auth controllers export const authController = new Auth(httpClient)
Дальнейшее взаимодействие зависит от вашего подхода к работе с запросами и используемого фреймворка или библиотеки. В React это может быть react-query, rtk-query и другие. В следующей статье я подробнее разберу, как интегрировать кодген с кешированием через react-query.
Посмотрим на практический пример работы с сгенерированными контроллерами.
Видео версия:
Если вы не хотите импортировать типы и контроллеры из внешней папки, вы можете объявлять типы на уровне сущности или модуля, используя реэкспорт или расширение/объединение типов.
Вот так я автоматизирую генерацию API-контроллеров на проектах Unistory. Планирую и дальше публиковать на Хабре такие гайды. Надеюсь, эта статья была полезной
ссылка на оригинал статьи https://habr.com/ru/articles/893150/
Добавить комментарий