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

от автора

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


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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *