2026-04-01 19:21:34 +03:00
# @gromlab/api-codegen
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
CLI для генерации typed TypeScript REST SDK из OpenAPI спецификации.
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
Генератор рассчитан на сценарий, где REST API можно вынести в отдельный npm-пакет, а приложения будут собирать из него свои API-клиенты: полный клиент, частичный клиент или точечные вызовы отдельных операций.
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
## Главная Идея
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
По умолчанию `@gromlab/api-codegen` генерирует split-клиент.
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
Каждый endpoint становится отдельной typed operation-функцией. Это позволяет импортировать только нужные методы и не тащить весь API в bundle приложения.
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
Если нужен полный клиент, используйте `operationsTree` .
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
Если нужен клиент только под конкретный экран, модуль или приложение, соберите дерево операций вручную.
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
Если нужен максимально точечный вызов, импортируйте одну operation напрямую.
2026-04-01 19:21:34 +03:00
2026-06-30 10:46:15 +03:00
## Быстрый Старт
2026-06-30 07:59:52 +03:00
2026-06-30 10:46:15 +03:00
```bash
npx @gromlab/api -codegen -i ./openapi.json -o ./src/generated
2026-06-30 07:59:52 +03:00
```
2026-06-30 10:46:15 +03:00
Для генерации из URL:
2026-06-30 07:59:52 +03:00
2026-06-30 10:46:15 +03:00
```bash
npx @gromlab/api -codegen -i https://api.example.com/openapi.json -o ./src/generated
2025-10-26 22:30:58 +03:00
```
2026-06-30 10:46:15 +03:00
По умолчанию будет создан split-клиент.
## Что Генерируется
2026-06-30 07:59:52 +03:00
```text
generated/
├── create-api-client.ts
├── data-contracts.ts
├── http-client.ts
├── index.ts
2026-06-30 10:46:15 +03:00
├── operations-tree.ts
2026-06-30 07:59:52 +03:00
└── operations/
├── index.ts
├── get-users.ts
└── create-user.ts
```
2026-06-30 10:46:15 +03:00
Основные части:
2026-06-30 07:59:52 +03:00
2026-06-30 10:46:15 +03:00
- `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.
2026-06-30 07:59:52 +03:00
2026-06-30 10:46:15 +03:00
## Полный Клиент
2026-06-30 07:59:52 +03:00
2026-06-30 10:46:15 +03:00
Если приложению нужен весь API, используйте `operationsTree` .
2025-10-26 22:30:58 +03:00
```typescript
2026-06-30 10:46:15 +03:00
import { createApiClient, HttpClient, operationsTree } from './generated';
// 1. Инициализация HTTP-клиента: baseUrl, headers, авторизация и transport-настройки.
const http = new HttpClient({
baseUrl: 'https://api.example.com',
});
2026-06-30 07:59:52 +03:00
2026-06-30 10:46:15 +03:00
// 2. Создание API-клиента: привязываем все сгенерированные операции к HTTP-клиенту.
const api = createApiClient(http, operationsTree);
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
// 3. Использование API-клиента: вызываем методы из дерева операций.
2026-06-30 07:59:52 +03:00
const users = await api.users.getAll({});
2026-06-30 10:46:15 +03:00
const createdUser = await api.users.create({ email, password });
2026-06-30 07:59:52 +03:00
```
2026-06-30 10:46:15 +03:00
`operationsTree` намеренно лежит в отдельном файле и экспортируется отдельно. Импортируя е г о , вы явно выбираете полный клиент с о всеми операциями.
## Частичный Клиент
Если приложению нужна только часть API, соберите клиент вручную.
2026-06-30 07:59:52 +03:00
```typescript
import { createApiClient, HttpClient } from './generated';
2026-06-30 10:46:15 +03:00
import {
v1AdminPharmaciesCreate,
v1AdminPharmaciesList,
v1AdminPharmaciesProfileUpdate,
} from './generated/operations';
2026-06-30 07:59:52 +03:00
const http = new HttpClient({
baseUrl: 'https://api.example.com',
2026-06-30 10:46:15 +03:00
});
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 напрямую из е е файла:
```typescript
import { v1AdminPharmaciesList } from './generated/operations/v1-admin-pharmacies-list';
```
## Точечный Вызов Operation
Если по какой-то причине нужно вызвать только одну операцию, можно не собирать API-клиент через `createApiClient` и вызвать operation напрямую.
```typescript
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 Клиент
2026-06-30 23:52:06 +03:00
`HttpClient` создается отдельно и передается в generated operations. Это позволяет централизованно настраивать `baseUrl` , headers, авторизацию, timeout и transport behavior.
2026-06-30 10:46:15 +03:00
```typescript
2026-06-30 23:52:06 +03:00
import { ApiError, HttpClient } from './generated';
2026-06-30 10:46:15 +03:00
2026-06-30 23:52:06 +03:00
const http = new HttpClient({
2026-06-30 10:46:15 +03:00
baseUrl: 'https://api.example.com',
2026-06-30 23:52:06 +03:00
credentials: 'include',
timeout: 10000,
headers: {
'X-App-Version': '1.0.0',
2026-06-30 10:46:15 +03:00
},
2026-06-30 23:52:06 +03:00
onRequest: (params) => {
const token = localStorage.getItem('access_token');
if (!params.secure || !token) {
return params;
}
2026-07-01 00:13:18 +03:00
const headers = new Headers(params.headers);
if (!headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${token}` );
}
2026-06-30 23:52:06 +03:00
return {
...params,
2026-07-01 00:13:18 +03:00
headers,
2026-06-30 23:52:06 +03:00
};
},
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` , поэтому авторизацию можно добавлять только там, где она нужна.
2026-07-01 00:13:18 +03:00
`onError` должен либо бросить ошибку, либо вернуть fallback-значение, либо вернуть результат `context.retry()` . Если вернуть `undefined` , ошибка будет считаться обработанной, а вызывающий код получит `undefined` вместо исключения.
2026-06-30 23:52:06 +03:00
### Опции 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. |
2026-07-01 00:13:18 +03:00
| `responseParser` | `(response, format) => unknown` | Кастомный парсинг response body. Ошибки парсинга успешных ответов попадают в `onError` . |
2026-06-30 23:52:06 +03:00
| `onRequest` | `(params, context) => params` | Request interceptor перед вызовом `fetch` . |
| `onResponse` | `(response, context) => response` | Response interceptor после успешного HTTP ответа. |
2026-07-01 00:13:18 +03:00
| `onError` | `(error, context) => result` | Error interceptor для HTTP ошибок, network errors, ошибок парсинга, retry и refresh-token сценариев. Должен вернуть fallback/retry или бросить ошибку. |
2026-06-30 23:52:06 +03:00
Также можно использовать другие стандартные поля `RequestInit` , доступные в вашей TypeScript/Runtime среде, если они не относятся к конкретному request body или cancellation lifecycle.
Эти поля не задаются на уровне конструктора и передаются конкретной operation или самому `request` :
| Поле | Почему не в конструкторе |
| --- | --- |
| `path` | Генерируется из OpenAPI path для конкретной operation. |
| `method` | Генерируется из OpenAPI HTTP метода. |
| `query` | Уникален для конкретного вызова operation. |
| `body` | Уникален для конкретного запроса. |
| `signal` | Один общий `AbortSignal` может случайно отменить группу независимых запросов. |
| `cancelToken` | Всегда относится к одному конкретному запросу. |
### Рецепты Кастомизации
2026-07-01 00:13:18 +03:00
Авторизация через `onRequest` без перезаписи явно переданного `Authorization` :
2026-06-30 23:52:06 +03:00
```typescript
const http = new HttpClient({
onRequest: (params) => {
const token = localStorage.getItem('access_token');
if (!params.secure || !token) {
return params;
2026-06-30 07:59:52 +03:00
}
2026-07-01 00:13:18 +03:00
const headers = new Headers(params.headers);
if (!headers.has('Authorization')) {
headers.set('Authorization', `Bearer ${token}` );
}
2026-06-30 07:59:52 +03:00
return {
2026-06-30 23:52:06 +03:00
...params,
2026-07-01 00:13:18 +03:00
headers,
2026-06-30 07:59:52 +03:00
};
},
});
2026-06-30 23:52:06 +03:00
```
Refresh token и повтор запроса:
```typescript
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` :
```typescript
const http = new HttpClient({
onResponse: (response, context) => {
console.log(context.request.method, context.url, response.status);
return response;
},
});
```
Кастомная сериализация query params:
```typescript
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` :
```typescript
const http = new HttpClient({
customFetch: async (url, init) => {
console.log('request', url);
return fetch(url, init);
},
});
```
Кастомный парсинг ответа:
2026-06-30 07:59:52 +03:00
2026-06-30 23:52:06 +03:00
```typescript
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();
},
});
2026-06-30 10:46:15 +03:00
```
Для отдельного вызова можно передать дополнительные request params последним аргументом operation.
```typescript
await api.users.getAll(
{},
{
headers: {
'X-Request-Id': requestId,
},
2026-06-30 07:59:52 +03:00
},
2026-06-30 10:46:15 +03:00
);
```
## API Как npm-Пакет
Типичный workflow:
1. Backend публикует OpenAPI спецификацию.
2. Отдельный пакет генерирует typed REST SDK в `src/generated` .
3. Пакет экспортирует generated entrypoint.
4. Приложения импортируют SDK и собирают нужные API-клиенты.
Пример структуры пакета:
```text
packages/my-api/
├── package.json
└── src/
├── generated/
└── index.ts
```
`src/index.ts` :
```typescript
export * from './generated';
```
Использование в приложении:
```typescript
import { createApiClient, HttpClient, operationsTree } from '@company/my -api';
const http = new HttpClient({
baseUrl: 'https://api.example.com',
2026-06-30 07:59:52 +03:00
});
2026-06-30 10:46:15 +03:00
export const api = createApiClient(http, operationsTree);
2026-06-30 07:59:52 +03:00
```
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
Если приложение хочет выбрать только часть API:
2025-10-26 22:30:58 +03:00
2026-06-30 07:59:52 +03:00
```typescript
2026-06-30 10:46:15 +03:00
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
Режим по умолчанию.
2025-10-26 22:30:58 +03:00
2026-06-30 10:46:15 +03:00
```bash
api-codegen -i ./openapi.json -o ./src/generated
2025-10-26 22:30:58 +03:00
```
2026-06-30 10:46:15 +03:00
Эквивалентно:
```bash
api-codegen -i ./openapi.json -o ./src/generated --mode split
```
2025-10-28 09:58:44 +03:00
2026-06-30 10:46:15 +03:00
Используйте этот режим для новых проектов, npm SDK пакетов и приложений, где важны модульность и tree-shaking.
### single
Legacy-режим, который генерирует один монолитный TypeScript файл.
2026-06-30 07:59:52 +03:00
2025-10-28 09:58:44 +03:00
```bash
2026-06-30 10:46:15 +03:00
api-codegen -i ./openapi.json -o ./src/generated --mode single -n MyApi
```
Результат:
```text
generated/
└── MyApi.ts
2025-10-28 09:58:44 +03:00
```
2026-06-30 10:46:15 +03:00
Используйте `single` , если проект уже завязан на старый монолитный generated-клиент.
## CLI
2025-10-28 09:58:44 +03:00
```bash
2026-06-30 10:46:15 +03:00
api-codegen -i < input > -o < output > [--mode split|single] [-n < name > ]
2025-10-28 09:58:44 +03:00
```
2026-06-30 10:46:15 +03:00
Аргументы:
- `-i, --input <path>` - путь к OpenAPI файлу или URL.
- `-o, --output <path>` - директория для generated файлов.
- `--mode <mode>` - режим генерации: `split` или `single` . По умолчанию `split` .
- `-n, --name <name>` - имя файла для `single` режима без расширения `.ts` .
- `--single-file` - устаревший алиас для `--mode single` .
## Примеры Команд
Split из локального файла:
```bash
api-codegen -i ./openapi.json -o ./src/generated
```
Split из URL:
```bash
api-codegen -i https://api.example.com/openapi.json -o ./src/generated
```
Legacy single файл:
```bash
api-codegen -i ./openapi.json -o ./src/generated --mode single -n MyApi
```
2025-10-28 09:58:44 +03:00
2025-10-26 22:30:58 +03:00
## Лицензия
MIT