Files
api-codegen/README.md

472 lines
18 KiB
Markdown
Raw Normal View 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 напрямую.
## Быстрый Старт
```bash
npx @gromlab/api-codegen -i ./openapi.json -o ./src/generated
```
Для генерации из URL:
```bash
npx @gromlab/api-codegen -i https://api.example.com/openapi.json -o ./src/generated
```
По умолчанию будет создан split-клиент.
## Что Генерируется
```text
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`.
```typescript
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, соберите клиент вручную.
```typescript
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 напрямую из ее файла:
```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 Клиент
`HttpClient` создается отдельно и передается в generated operations. Это позволяет централизованно настраивать `baseUrl`, headers, авторизацию, timeout и transport behavior.
```typescript
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`:
```typescript
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 и повтор запроса:
```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);
},
});
```
Кастомный парсинг ответа:
```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();
},
});
```
Для отдельного вызова можно передать дополнительные request params последним аргументом operation.
```typescript
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-клиенты.
Пример структуры пакета:
```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',
});
export const api = createApiClient(http, operationsTree);
```
Если приложение хочет выбрать только часть API:
```typescript
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
Режим по умолчанию.
```bash
api-codegen -i ./openapi.json -o ./src/generated
```
Эквивалентно:
```bash
api-codegen -i ./openapi.json -o ./src/generated --mode split
```
2025-10-28 09:58:44 +03:00
Используйте этот режим для новых проектов, npm SDK пакетов и приложений, где важны модульность и tree-shaking.
### single
Legacy-режим, который генерирует один монолитный TypeScript файл.
2025-10-28 09:58:44 +03:00
```bash
api-codegen -i ./openapi.json -o ./src/generated --mode single -n MyApi
```
Результат:
```text
generated/
└── MyApi.ts
2025-10-28 09:58:44 +03:00
```
Используйте `single`, если проект уже завязан на старый монолитный generated-клиент.
## CLI
2025-10-28 09:58:44 +03:00
```bash
api-codegen -i <input> -o <output> [--mode split|single] [-n <name>]
2025-10-28 09:58:44 +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
## Лицензия
MIT