Как стать автором
Обновить

Авто-генерация типизированных API контроллеров на клиенте из Swagger

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров673

Привет! Меня зовут Данил, я 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

  1. Получаем API спецификацию → На странице сваггера находим OpenAPI JSON-схему. Как правило, она находится под названием проекта.

    Снимок экрана 2025-03-10 в 14.52.44.png
    Снимок экрана 2025-03-10 в 14.52.44.png

    Если схемы нет, бекендер должен её описать в коде, используя инструменты типа NestJS Swagger, FastAPI, SpringDoc или другие подходящие для его стека. Без этого генерация API-клиента невозможна.

    Также в данной реализации важно понимать, что при обновлениях бекенда, необходимо каждый раз обновлять спеку на клиенте и выполнять команду npm run codegen, но если статья вам покажется интересной, я обязательно напишу продолжение, в котором реализую автоматическое подтягивание спеки.

  2. Выгружаем 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"
}
  1. Запускаем скрипт слияния и генерации → Для удобства я создал скрипт, который объединяет две команды в одну: 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 и передать его в контроллеры, обеспечивая единую точку конфигурации для запросов в проекте.

  2. Объявляем 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,
      // Другие параметры по необходимости
    })
    
  3. Объявляем 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)
      

Что мы имеем?

Представим, что наш сваггер выглядел так:

Снимок экрана 2025-03-11 в 12.13.07.png
Снимок экрана 2025-03-11 в 12.13.07.png

После всех вышеперечисленных действий в папке 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)

Описание файлов:

  1. Admins.ts, Auth.ts, CounterNftLotOffers.ts, ..., Users.ts

    • Каждый файл представляет API-контроллер для соответствующей сущности.

    • Содержит классы и методы для запросов (GET, POST, PUT, DELETE).

    • Использует HTTP-клиент для выполнения запросов.

  2. data-contracts.ts

    • Описывает типы данных (интерфейсы) для запросов и ответов API.

    • Используется во всех контроллерах.

  3. http-client.ts

    • Отвечает за отправку HTTP-запросов.

    • Может содержать кастомную логику для работы с токенами и авторизацией.

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

Теги:
Хабы:
+1
Комментарии4

Публикации

Истории

Работа

Ближайшие события

4 – 5 апреля
Геймтон «DatsCity»
Онлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань
20 – 22 июня
Летняя айти-тусовка Summer Merge
Ульяновская область