# @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 -o [--mode split|single] [-n ] ``` Аргументы: - `-i, --input ` - путь к OpenAPI файлу или URL. - `-o, --output ` - директория для generated файлов. - `--mode ` - режим генерации: `split` или `single`. По умолчанию `split`. - `-n, --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