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

472 lines
18 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @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
```
Используйте этот режим для новых проектов, npm SDK пакетов и приложений, где важны модульность и tree-shaking.
### single
Legacy-режим, который генерирует один монолитный TypeScript файл.
```bash
api-codegen -i ./openapi.json -o ./src/generated --mode single -n MyApi
```
Результат:
```text
generated/
└── MyApi.ts
```
Используйте `single`, если проект уже завязан на старый монолитный generated-клиент.
## CLI
```bash
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 из локального файла:
```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
```
## Лицензия
MIT