На связи снова Архитектурный комитет компании SimbirSoft, и мы продолжаем наш цикл статей, посвященных Design API First. Ранее мы уже писали о том, что представляет собой этот подход, приводили пример спецификации для сервиса аутентификации и рассказывали, как мы интегрируем этот паттерн в наш конвейер разработки.

Сегодня мы немного отвлечемся от бэкенда и разберем автоматизацию одной из рутинных задач на стороне frontend-разработки, а именно — описание моделей интерфейсов для взаимодействия фронта с беком, также написание API-сервисов, в которых фиксируются endpoints, методы запросов и формат передачи данных (query-параметры, заголовки, тело).

Инструменты кодогенерации

В нашей компании активно применяется стандарт OpenAPI. Несмотря на свою объемность в сравнении с более молодыми форматами, он является проверенным и наиболее отлаженным. Для него существует множество инструментов для поддержания документации. Поэтому рассмотрим несколько популярных инструментов, позволяющих работать с этим стандартом:

  1. swagger-typescript-api — генерирует модели и сервисы (Fetch или Axios), есть возможность написания своих шаблонов кодогенерации.

  2. openapi-typescript очень легковесный инструмент, заточенный под фронтендера, с хорошей поддержкой и без лишних зависимостей. Раскладывает спецификацию на набор интерфейсов, далее либо применяем самостоятельно данный инструмент, либо пользуемся вспомогательными — openapi-fetch или openapi-typescript-fetch, которые примут в качестве дженерика полученные ранее интерфейсы. Клиентов для axios или angular под openapi-typescript, к сожалению, не существует.

  3. openapi-typescript-codegen – инструмент, аналогичный предыдущему, но дающий чуть больше кастомизации для шаблонов, в том числе тут есть шаблоны клиентов под axios или angular.

  4. openapitools/openapi-generator-cli — это по сути Node.js-обертка над Swagger Codegen, который поддерживается создателями Swagger и стандарта OpenApi. Невероятно мощный инструмент: может генерировать клиентов не только для frontend, но и для backend. Есть возможность передачи кастомных шаблонов, можно настроить кодогенерацию под конкретный формат клиентов (Fetch, Angular, RxJS и другие).

Рисунок 1. Популярные npm-пакеты для кодогенерации
Рисунок 1. Популярные npm-пакеты для кодогенерации

Из npm-трендов (Рис. 1) видно, что openapi-typescript догоняет по популярности openapitools/openapi-generator-cli. Почему так происходит?

Во-первых, последний инструмент в зависимостях имеет Java — frontend-разработчику приходится ставить JRE или запускать генерацию в докере. Для запуска в докере есть специальный флаг в конфиге генератора, поэтому писать докер-файл не нужно, все запускается «под капотом». 

Во-вторых, openapi-typescript предоставляет лишь корректную конвертацию в интерфейсы ТS, дальше с ними можно сделать что угодно. В то время как последний инструмент генерит еще и клиентов, и, если они не подойдут, приходится изучать громоздкую документацию и перенастраивать шаблоны кодогенерации.

Разберем пример на основе openapi-typescript-codegen и спеки сервиса авторизации из первой статьи.

Выполняем команды:

$ npm i openapi-typescript-codegen
$ npx openapi --input=spec.yml --output=api --client=fetch

И получаем следующую структуру:

 .
└── api/
    ├── core/
    ├── models/
    ├── services/
    └── index.ts

Директория core содержит базовые переиспользуемые модели, связанные непосредственно с кодогенертором. Папка models содержит все упомянутые в спеке модели, services содержит клиенты (URL для endpoints в связке с нужными моделями). Примеры моделей и сервиса приведены ниже.

Примеры моделей
export type Auth_Request_Model_AuthAccount = {
  /**
   * Email, привязанный к аккаунту пользователя
   */
  userLogin: string;
};
export type Auth_Response_Model_AbstractSuccessAccessAccount = {
    accessToken: string;
    /**
     * ID аккаунта пользователя
     */
    accountId?: string;
    message: string;
};

Пример сервиса
import type {Auth_Request_Model_AuthAccount} from "../models/Auth_Request_Model_AuthAccount";
import type {Auth_Response_Model_Account} from "../models/Auth_Response_Model_Account";
import type {Auth_Response_Model_WaitingAccessAccount} from "../models/Auth_Response_Model_WaitingAccessAccount";
 
import type {CancelablePromise} from "../core/CancelablePromise";
import {OpenAPI} from "../core/OpenAPI";
import {request as __request} from "../core/request";
 
export class AuthService {
  /**
   * Auth/R2. Метод получения доступа к аккаунту пользователя
   * Метод предназначен для аутентификации пользователя под указанным email, соответствующим аккаунту в БД
   * @param requestBody
   * @param acceptLanguage https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Accept-Language
   * @param correlationId
   * @param platform
   * @returns Auth_Response_Model_WaitingAccessAccount - Ожидается активация созданного аккаунта пользователя
   * - Ожидается подтверждение входа пользователя в свой аккаунт
   * - Ожидается подтверждение восстановления доступа пользователя к своему аккаунту
   *
   * @throws ApiError
   */
  public static authAccount(requestBody: Auth_Request_Model_AuthAccount, acceptLanguage?: string, correlationId?: string, platform?: "MOBILE" | "WEB"): CancelablePromise<Auth_Response_Model_WaitingAccessAccount> {
    return __request(OpenAPI, {
      method: "POST",
      url: "/v1/signin",
      headers: {
        "Accept-Language": acceptLanguage,
        CorrelationID: correlationId,
        Platform: platform,
      },
      body: requestBody,
      mediaType: "application/json;charset=UTF-8",
      errors: {
        400: `Некорректные входные данные. Возвращается список атрибутов с ошибками`,
        403: `Пользователю с текущими правами доступ отклонён`,
        500: `Внутренняя ошибка сервера`,
      },
    });
  }
 
  /**
   * Auth/R3. Метод получения информации об аккаунте
   * Метод предназначен для получения информации из БД об аккаунте текущего пользователя
   * @param acceptLanguage https://developer.mozilla.org/ru/docs/Web/HTTP/Headers/Accept-Language
   * @param correlationId
   * @returns Auth_Response_Model_Account Сформирован ответ с информацией об аккаунте текущего пользователя
   * @throws ApiError
   */
  public static getAccount(acceptLanguage?: string, correlationId?: string): CancelablePromise<Auth_Response_Model_Account> {
    return __request(OpenAPI, {
      method: "GET",
      url: "/v1/auth",
      headers: {
        "Accept-Language": acceptLanguage,
        CorrelationID: correlationId,
      },
      errors: {
        401: `Пользователь не был аутентифицирован`,
        500: `Внутренняя ошибка сервера`,
      },
    });
  }
}

Рассмотрим несколько вариантов реализации инфраструктуры.

Разработка в монорепозитории

Поскольку для кодогенерации нужна спецификация, которую никто не хранит в директории с кодом frontend, то одним из естественных решений для такой связности является использование монорепозитория. Это в каком-то плане классический подход (Рис. 2).

Рисунок 2. Реализация в монорепозитории
Рисунок 2. Реализация в монорепозитории

Backend и frontend разрабатываются вместе, что дает удобный доступ к спецификации. При этом не важно, что первично: спецификация или код —  мы либо сразу генерируем модели, либо запускаем ещё промежуточную команду по экстракции спеки из кода.

Разработка в раздельных репозиториях

Кодогенерация моделей при хранении кода frontend и backend в раздельных репозиториях сильно зависит от выбранного воркфлоу на проекте.

1. С тестированием в общей ветке

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

1.1. Для случаев, когда тестирование делается в общей ветке, можно дождаться, когда сборку backend накатят на стенд Dev, отчего спецификация перезальется, и frontend-специалист сможет ее спокойно скачать по известному URL (Рис. 3).

Рисунок 3. Хранение спецификации в репозитории бекенда
Рисунок 3. Хранение спецификации в репозитории бекенда

1.2. Если на проекте проектированию отдельных кусков функциональности уделяется должное внимание, происходит согласование форматов взаимодействия frontend и backend. И/или есть сильная потребность запускать разработку параллельно (вести разработку frontend вместе с backend, а не после оного), то вам может потребовать отдельный репозиторий для хранения спецификаций. Отличия от предыдущего варианта минимальны (Рис. 4) — тут backend генерирует и для себя клиентов.

Рисунок 4. Хранение спецификации в отдельном репозитории
Рисунок 4. Хранение спецификации в отдельном репозитории

1.3. Если спецификация лежит статично и не нуждается в какой-либо компиляции, то можно «сходить» в соседний репозиторий и забрать ее оттуда, не дожидаясь деплоя на стенд (Рис. 5). Для авторизации curl-a в gitlab или bitbacket используем Access Token, сгенерированный под конкретного человека, в Git не храним (добавляем в gitignore).

Рисунок 5. Модифицированный способ хранения спецификации
Рисунок 5. Модифицированный способ хранения спецификации

2. С тестированием отдельных веток на отдельных стендах

Если перед вливанием в общую ветку ведется отдельное тестирование на отдельном стенде, то все становится интересней и сложней. Подходы 1.1-1.3 можно переиспользовать (и в 90% случаев их хватает), но уже в отношении отдельных стендов. В зависимости от задачи надо только не забывать менять URL на спецификацию.

Как вариант, для особо сложных случаев можно кодогенерацию вынести в CI, клиенты будут артефактами сборки. В дальнейшем клиенты публикуются в корпоративном npm. Для версионирования используются «версия продукта» + «имя ветки» (например, @project-scope/api@1.2.3-task_123). Если пакета с таким номером задачи не существует, то ставим по дефолту последний пакет из общей ветки (например, @project-scope/api@1.2.3-dev). Отметим, что ставить пакеты нужно с флагом «no-save», так как от задачи к задаче будут появляться новые версии пакетов (Рис. 6).

Рисунок 6. Вариант реализации с тестированием отдельных веток
Рисунок 6. Вариант реализации с тестированием отдельных веток

Заключение

Итак, в этой статье мы разобрали несколько инструментов кодогенерации, которые могут полезны frontend-разработчику, описали типовые схемы реализации инфраструктуры. Конечно, выбор определенной схемы зависит от конкретных задач, но вывод все равно очевиден: если вы используете на проекте подход Design API First, то пользуйтесь кодогенераторами — это позволит автоматизировать небольшую работу в пределах одной задачи, но огромную в рамках всего проекта. Также поможет избавиться от ошибок из-за человеческого фактора при интеграции с backend.

При наличии удобного доступа к спецификации пользуйтесь подходами 1.1-1.3 — просто забирайте ее из соседней директории или curl-ом.

В противном случае можно присмотреться к npm как к очень удобной системе доставке кода. Например, на проектах с микросервисами и микрофронтами.

Пользуетесь ли вы подобной кодогенерацией? Как доставляете спецификацию или готовый код на фронт? Какие интересные кейсы вам попадались? Пишите в комментариях, будем рады обсудить это вместе.

Другие полезные материалы для разработчиков и архитекторов в IT публикуем в наших соцсетях — ВК и Telegram

Комментарии (1)


  1. onegreyonewhite
    31.07.2023 18:33
    +1

    А мы вот на лету научились парсить схему и делать приложения практически без написания js/ts кода. Это я так, хвастаюсь. А если серьёзно, то это капец удобно. Очень много времени на разработку экономит. Но требует определённой дисциплины: стандартные интерфейсы, изначально качественный дизайн и т.п.

    Используем swagger-typescript-api, но там мерзкий баг с camelCase преобразованием из snake_case. Спасибо, что подкинули куда ещё посмотреть.