Files
api-codegen/README.md
S.Gromov 4ce5ea9b65 fix: исправить edge cases HttpClient
- ошибки парсинга успешных ответов проброшены в onError
- добавлена защита от перезаписи явного Authorization
- обновлены тесты, README и примеры кастомизации
2026-07-01 00:13:18 +03:00

18 KiB
Raw Blame History

@gromlab/api-codegen

CLI для генерации typed TypeScript REST SDK из OpenAPI спецификации.

Генератор рассчитан на сценарий, где REST API можно вынести в отдельный npm-пакет, а приложения будут собирать из него свои API-клиенты: полный клиент, частичный клиент или точечные вызовы отдельных операций.

Главная Идея

По умолчанию @gromlab/api-codegen генерирует split-клиент.

Каждый endpoint становится отдельной typed operation-функцией. Это позволяет импортировать только нужные методы и не тащить весь API в bundle приложения.

Если нужен полный клиент, используйте operationsTree.

Если нужен клиент только под конкретный экран, модуль или приложение, соберите дерево операций вручную.

Если нужен максимально точечный вызов, импортируйте одну operation напрямую.

Быстрый Старт

npx @gromlab/api-codegen -i ./openapi.json -o ./src/generated

Для генерации из URL:

npx @gromlab/api-codegen -i https://api.example.com/openapi.json -o ./src/generated

По умолчанию будет создан split-клиент.

Что Генерируется

generated/
├── create-api-client.ts
├── data-contracts.ts
├── http-client.ts
├── index.ts
├── operations-tree.ts
└── operations/
    ├── index.ts
    ├── get-users.ts
    └── create-user.ts

Основные части:

  • http-client.ts - fetch-based HTTP клиент, настройки авторизации, headers, baseUrl и transport customization.
  • data-contracts.ts - TypeScript типы из OpenAPI schemas.
  • operations/*.ts - отдельная typed function на каждый endpoint.
  • operations/index.ts - barrel export всех операций.
  • operations-tree.ts - дерево всех операций для сборки полного API клиента.
  • create-api-client.ts - helper, который привязывает дерево операций к конкретному HTTP клиенту.
  • index.ts - основная входная точка generated SDK.

Полный Клиент

Если приложению нужен весь API, используйте operationsTree.

import { createApiClient, HttpClient, operationsTree } from './generated';

// 1. Инициализация HTTP-клиента: baseUrl, headers, авторизация и transport-настройки.
const http = new HttpClient({
  baseUrl: 'https://api.example.com',
});

// 2. Создание API-клиента: привязываем все сгенерированные операции к HTTP-клиенту.
const api = createApiClient(http, operationsTree);

// 3. Использование API-клиента: вызываем методы из дерева операций.
const users = await api.users.getAll({});
const createdUser = await api.users.create({ email, password });

operationsTree намеренно лежит в отдельном файле и экспортируется отдельно. Импортируя его, вы явно выбираете полный клиент со всеми операциями.

Частичный Клиент

Если приложению нужна только часть API, соберите клиент вручную.

import { createApiClient, HttpClient } from './generated';
import {
  v1AdminPharmaciesCreate,
  v1AdminPharmaciesList,
  v1AdminPharmaciesProfileUpdate,
} from './generated/operations';

const http = new HttpClient({
  baseUrl: 'https://api.example.com',
});

const api = createApiClient(http, {
  pharmaciesAdmin: {
    list: v1AdminPharmaciesList,
    create: v1AdminPharmaciesCreate,
    updateProfile: v1AdminPharmaciesProfileUpdate,
  },
});

const pharmacies = await api.pharmaciesAdmin.list({});
const createdPharmacy = await api.pharmaciesAdmin.create(payload);
await api.pharmaciesAdmin.updateProfile({ id }, payload);

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

operations/index.ts реэкспортит все operation-функции, поэтому можно делать named imports из ./generated/operations. В ESM-сборке такой импорт остается tree-shaking friendly: в клиенте используются только явно выбранные операции.

Если OpenAPI большой и полный SDK занимает десятки тысяч строк, приложение не обязано тянуть весь набор методов в свой чанк. Можно собрать минимально рабочий API-клиент под конкретный сценарий.

Если нужен максимально строгий контроль над тем, какой файл попадет в bundle, импортируйте operation напрямую из ее файла:

import { v1AdminPharmaciesList } from './generated/operations/v1-admin-pharmacies-list';

Точечный Вызов Operation

Если по какой-то причине нужно вызвать только одну операцию, можно не собирать API-клиент через createApiClient и вызвать operation напрямую.

import { HttpClient } from './generated/http-client';
import { v1AdminPharmaciesList } from './generated/operations/v1-admin-pharmacies-list';

const http = new HttpClient({
  baseUrl: 'https://api.example.com',
});

const pharmacies = await v1AdminPharmaciesList(http, {});

Это полезно для разовых сценариев: health-check, bootstrap-запроса, утилитного скрипта или кода, где полноценный объект api не нужен.

Кастомный HTTP Клиент

HttpClient создается отдельно и передается в generated operations. Это позволяет централизованно настраивать baseUrl, headers, авторизацию, timeout и transport behavior.

import { ApiError, HttpClient } from './generated';

const http = new HttpClient({
  baseUrl: 'https://api.example.com',
  credentials: 'include',
  timeout: 10000,
  headers: {
    'X-App-Version': '1.0.0',
  },
  onRequest: (params) => {
    const token = localStorage.getItem('access_token');

    if (!params.secure || !token) {
      return params;
    }

    const headers = new Headers(params.headers);

    if (!headers.has('Authorization')) {
      headers.set('Authorization', `Bearer ${token}`);
    }

    return {
      ...params,
      headers,
    };
  },
  onResponse: (response) => response,
  onError: async (error, context) => {
    if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
      await refreshToken();
      return context.retry();
    }

    throw error;
  },
});

onRequest вызывается перед fetch, onResponse после успешного ответа, onError для HTTP-ошибок, network errors и ошибок парсинга. Для защищенных endpoints generated operation передает secure: true, поэтому авторизацию можно добавлять только там, где она нужна.

onError должен либо бросить ошибку, либо вернуть fallback-значение, либо вернуть результат context.retry(). Если вернуть undefined, ошибка будет считаться обработанной, а вызывающий код получит undefined вместо исключения.

Опции HttpClient

HttpClient принимает плоский конфиг. Стандартные fetch-опции можно задавать прямо в конструкторе вместе с кастомными hooks клиента.

Опция Тип Назначение
baseUrl string Базовый URL API. По умолчанию берется из servers[0].url OpenAPI спецификации.
headers HeadersInit Заголовки по умолчанию для всех запросов.
credentials RequestCredentials Политика отправки cookies/auth credentials: omit, same-origin, include.
mode RequestMode Fetch request mode, например cors, same-origin, no-cors.
cache RequestCache Fetch cache policy.
redirect RequestRedirect Поведение при redirect: follow, error, manual.
referrer string Значение referrer для запросов.
referrerPolicy ReferrerPolicy Политика referrer.
integrity string Subresource integrity value.
keepalive boolean Позволяет запросу пережить unload страницы.
secure boolean Дефолтный маркер защищенного запроса. Обычно выставляется generated operation по OpenAPI security.
type ContentType Дефолтный Content-Type для body. Обычно выставляется generated operation.
format ResponseFormat Дефолтный способ парсинга ответа: json, text, blob, formData, arrayBuffer.
timeout number Таймаут запроса в миллисекундах. Работает через AbortSignal.
customFetch typeof fetch Замена стандартного fetch, например для тестов, SSR или custom transport.
paramsSerializer (query) => string Кастомная сериализация query params в URL.
responseParser (response, format) => unknown Кастомный парсинг response body. Ошибки парсинга успешных ответов попадают в onError.
onRequest (params, context) => params Request interceptor перед вызовом fetch.
onResponse (response, context) => response Response interceptor после успешного HTTP ответа.
onError (error, context) => result Error interceptor для HTTP ошибок, network errors, ошибок парсинга, retry и refresh-token сценариев. Должен вернуть fallback/retry или бросить ошибку.

Также можно использовать другие стандартные поля RequestInit, доступные в вашей TypeScript/Runtime среде, если они не относятся к конкретному request body или cancellation lifecycle.

Эти поля не задаются на уровне конструктора и передаются конкретной operation или самому request:

Поле Почему не в конструкторе
path Генерируется из OpenAPI path для конкретной operation.
method Генерируется из OpenAPI HTTP метода.
query Уникален для конкретного вызова operation.
body Уникален для конкретного запроса.
signal Один общий AbortSignal может случайно отменить группу независимых запросов.
cancelToken Всегда относится к одному конкретному запросу.

Рецепты Кастомизации

Авторизация через onRequest без перезаписи явно переданного Authorization:

const http = new HttpClient({
  onRequest: (params) => {
    const token = localStorage.getItem('access_token');

    if (!params.secure || !token) {
      return params;
    }

    const headers = new Headers(params.headers);

    if (!headers.has('Authorization')) {
      headers.set('Authorization', `Bearer ${token}`);
    }

    return {
      ...params,
      headers,
    };
  },
});

Refresh token и повтор запроса:

const http = new HttpClient({
  onError: async (error, context) => {
    if (error instanceof ApiError && error.status === 401 && context.retryCount === 0) {
      await refreshToken();
      return context.retry();
    }

    throw error;
  },
});

Логирование ответов через onResponse:

const http = new HttpClient({
  onResponse: (response, context) => {
    console.log(context.request.method, context.url, response.status);
    return response;
  },
});

Кастомная сериализация query params:

const http = new HttpClient({
  paramsSerializer: (query) => {
    const params = new URLSearchParams();

    Object.entries(query).forEach(([key, value]) => {
      if (Array.isArray(value)) {
        params.set(key, value.join(','));
        return;
      }

      if (value !== undefined) {
        params.set(key, String(value));
      }
    });

    return params.toString();
  },
});

Замена транспорта через customFetch:

const http = new HttpClient({
  customFetch: async (url, init) => {
    console.log('request', url);
    return fetch(url, init);
  },
});

Кастомный парсинг ответа:

const http = new HttpClient({
  responseParser: async (response) => {
    if (response.status === 204) {
      return undefined;
    }

    const contentType = response.headers.get('content-type') || '';

    if (contentType.includes('json')) {
      return response.json();
    }

    return response.text();
  },
});

Для отдельного вызова можно передать дополнительные request params последним аргументом operation.

await api.users.getAll(
  {},
  {
    headers: {
      'X-Request-Id': requestId,
    },
  },
);

API Как npm-Пакет

Типичный workflow:

  1. Backend публикует OpenAPI спецификацию.
  2. Отдельный пакет генерирует typed REST SDK в src/generated.
  3. Пакет экспортирует generated entrypoint.
  4. Приложения импортируют SDK и собирают нужные API-клиенты.

Пример структуры пакета:

packages/my-api/
├── package.json
└── src/
    ├── generated/
    └── index.ts

src/index.ts:

export * from './generated';

Использование в приложении:

import { createApiClient, HttpClient, operationsTree } from '@company/my-api';

const http = new HttpClient({
  baseUrl: 'https://api.example.com',
});

export const api = createApiClient(http, operationsTree);

Если приложение хочет выбрать только часть API:

import { createApiClient, HttpClient } from '@company/my-api';
import {
  v1AdminPharmaciesList,
  v1AdminPharmaciesProfileUpdate,
} from '@company/my-api/operations';

export const api = createApiClient(new HttpClient(), {
  pharmaciesAdmin: {
    list: v1AdminPharmaciesList,
    updateProfile: v1AdminPharmaciesProfileUpdate,
  },
});

Режимы Генерации

split

Режим по умолчанию.

api-codegen -i ./openapi.json -o ./src/generated

Эквивалентно:

api-codegen -i ./openapi.json -o ./src/generated --mode split

Используйте этот режим для новых проектов, npm SDK пакетов и приложений, где важны модульность и tree-shaking.

single

Legacy-режим, который генерирует один монолитный TypeScript файл.

api-codegen -i ./openapi.json -o ./src/generated --mode single -n MyApi

Результат:

generated/
└── MyApi.ts

Используйте single, если проект уже завязан на старый монолитный generated-клиент.

CLI

api-codegen -i <input> -o <output> [--mode split|single] [-n <name>]

Аргументы:

  • -i, --input <path> - путь к OpenAPI файлу или URL.
  • -o, --output <path> - директория для generated файлов.
  • --mode <mode> - режим генерации: split или single. По умолчанию split.
  • -n, --name <name> - имя файла для single режима без расширения .ts.
  • --single-file - устаревший алиас для --mode single.

Примеры Команд

Split из локального файла:

api-codegen -i ./openapi.json -o ./src/generated

Split из URL:

api-codegen -i https://api.example.com/openapi.json -o ./src/generated

Legacy single файл:

api-codegen -i ./openapi.json -o ./src/generated --mode single -n MyApi

Лицензия

MIT