- добавлен плоский ApiConfig с lifecycle hooks - добавлены ApiError, retry context, timeout и кастомные parser/serializer - обновлены примеры, документация и тесты под новый API
@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;
}
return {
...params,
headers: {
...params.headers,
Authorization: `Bearer ${token}`,
},
};
},
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, поэтому авторизацию можно добавлять только там, где она нужна.
Опции 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. |
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 сценариев. |
Также можно использовать другие стандартные поля RequestInit, доступные в вашей TypeScript/Runtime среде, если они не относятся к конкретному request body или cancellation lifecycle.
Эти поля не задаются на уровне конструктора и передаются конкретной operation или самому request:
| Поле | Почему не в конструкторе |
|---|---|
path |
Генерируется из OpenAPI path для конкретной operation. |
method |
Генерируется из OpenAPI HTTP метода. |
query |
Уникален для конкретного вызова operation. |
body |
Уникален для конкретного запроса. |
signal |
Один общий AbortSignal может случайно отменить группу независимых запросов. |
cancelToken |
Всегда относится к одному конкретному запросу. |
Рецепты Кастомизации
Авторизация через onRequest:
const http = new HttpClient({
onRequest: (params) => {
const token = localStorage.getItem('access_token');
if (!params.secure || !token) {
return params;
}
return {
...params,
headers: {
...params.headers,
Authorization: `Bearer ${token}`,
},
};
},
});
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:
- Backend публикует OpenAPI спецификацию.
- Отдельный пакет генерирует typed REST SDK в
src/generated. - Пакет экспортирует generated entrypoint.
- Приложения импортируют 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