
Привет! Меня зовут Данил, я 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. Планирую и дальше публиковать на Хабре такие гайды. Надеюсь, эта статья была полезной ;-)