Files

343 lines
15 KiB
Markdown
Raw Permalink Normal View History

2026-08-01 09:31:08 +03:00
---
title: Автогенерация REST-клиента
description: Генерация split REST-клиента из OpenAPI-спецификации.
keywords: [rest, openapi, api-codegen, автогенерация, generated, split, operations, npx]
---
# Автогенерация REST-клиента
Генерация REST-клиента из OpenAPI-спецификации.
## Когда использовать
Автогенерация используется, когда у API есть актуальная OpenAPI-спецификация. Генератор создаёт split-клиент: HTTP-клиент, типы и отдельную operation-функцию на каждый endpoint. Разработчик вручную добавляет транспорт в `client.ts`, полный API в `rest-api.ts` и GET-хуки.
По умолчанию генерация идёт внутрь infra-модуля приложения в `{name}-rest-api/generated` — этот сценарий описан ниже. Если клиент нужен нескольким приложениям, generated-код выносится в отдельный пакет `{name}-rest-api-sdk`: [SDK-пакет REST-клиента](./sdk.md).
## Пример API
В примерах используется Swagger Petstore:
```text
https://petstore3.swagger.io/api/v3/openapi.json
```
Имена модуля:
```text
src/infra/pet-store-rest-api/
petStoreRestApi
```
## Скрипт генерации
`@gromlab/api-codegen` не устанавливается в `devDependencies`. Используем `npx @gromlab/api-codegen@latest`, чтобы запускать свежую версию.
```json
{
"scripts": {
"codegen:pet-store-rest-api": "npx @gromlab/api-codegen@latest -i https://petstore3.swagger.io/api/v3/openapi.json -o src/infra/pet-store-rest-api/generated"
}
}
```
Параметры:
- `-i` — путь к OpenAPI-спецификации: URL или локальный файл.
- `-o` — директория для сгенерированного split-клиента.
По умолчанию генератор работает в режиме `split`. Legacy-режим `--mode single -n <имя>` генерирует один монолитный файл и используется только в старых проектах, которые уже завязаны на монолитный generated-клиент. В новых проектах single-режим не используется.
Генератор не создаёт SWR-хуки. GET-хуки REST-клиента пишутся вручную, чтобы сохранить проектный контракт: один GET-хук = одна GET operation, без бизнес-логики и композиции.
## Генерация
```bash
npm run codegen:pet-store-rest-api
```
Ожидаемый результат:
```text
src/infra/pet-store-rest-api/generated/
├── create-api-client.ts
├── data-contracts.ts
├── http-client.ts
├── index.ts
├── operations-tree.ts
└── operations/
├── index.ts
├── get-pet-by-id.ts
└── find-pets-by-status.ts
```
Основные части:
- `http-client.ts` — fetch-based `HttpClient`: `baseUrl`, заголовки, авторизация и хуки транспорта.
- `data-contracts.ts` — TypeScript-типы из OpenAPI schemas, включая `*Params`-типы операций.
- `operations/*.ts` — отдельная typed operation-функция на каждый endpoint.
- `operations-tree.ts` — дерево всех операций для сборки полного клиента.
- `create-api-client.ts``createApiClient`, который привязывает дерево операций к `HttpClient`.
- `index.ts` — входная точка generated-кода, реэкспортирует всё перечисленное.
Файлы в `generated/` не правятся руками и коммитятся в репозиторий.
Enum-значения в split-режиме генерируются как union-типы строковых литералов. Отдельных runtime enum в `generated/` нет: в коде используются строковые литералы вроде `'available'`, а тип проверяет их допустимость.
## Проверка операций
После генерации откройте `generated/operations-tree.ts` и проверьте фактические имена operation-функций и структуру дерева.
Для Petstore нужны GET-операции вида:
```ts
import { findPetsByStatus } from './generated/operations/find-pets-by-status'
import { getPetById } from './generated/operations/get-pet-by-id'
```
Точечная operation вызывается через настроенный `HttpClient`:
```ts
getPetById(petStoreHttpClient, { petId: 10 })
findPetsByStatus(petStoreHttpClient, { status: 'available' })
```
После сборки полного клиента в `rest-api.ts` те же операции доступны как bound API:
```ts
petStoreRestApi.pet.findPetsByStatus({ status: 'available' })
petStoreRestApi.pet.getPetById({ petId: 10 })
```
Имена операций и группировка дерева зависят от `operationId` и тегов OpenAPI-схемы. В рабочих задачах всегда сверяйтесь с `generated/operations-tree.ts` и `generated/data-contracts.ts`.
## Источник Imports
Если generated-код лежит внутри infra-модуля, импортируйте operation из локального `generated/`:
```ts
import { getPetById } from '../generated/operations/get-pet-by-id'
```
Если generated-код вынесен в SDK-пакет, импортируйте operation через subpath export пакета:
```ts
import { getPetById } from '@company/pet-store-rest-api-sdk/operations/get-pet-by-id'
```
Не импортируйте весь `operations` namespace ради одной операции.
## Алгоритм для агента
После генерации агент должен действовать по шагам:
1. Открыть `generated/operations-tree.ts` и найти фактические имена нужных operation-функций.
2. Для каждой нужной операции найти тип параметров и тип ответа в `generated/data-contracts.ts`.
3. Создать или обновить `client.ts`: настроить и экспортировать только `*HttpClient`.
4. Создать или обновить `rest-api.ts`: собрать полный `*RestApi` через `createApiClient(*HttpClient, operationsTree)`.
5. Создать GET-хуки только для реально нужных GET-операций, не для всех операций API на всякий случай.
6. В каждом GET-хуке импортировать точечную operation из `operations/<operation-file>`.
7. Для каждого GET-хука создать key-функцию формата `[serviceName, endpoint]`.
8. В key-функции вернуть `null`, если обязательные параметры не готовы.
9. В хуке принять `params?: GeneratedParams | null` и `config?: SWRConfiguration<Data>`.
10. В fetcher вызвать operation через общий `*HttpClient`: `operation(nameHttpClient, params as GeneratedParams, requestParams?)`.
11. Экспортировать хук и key-функцию из `hooks/index.ts`; если это первый hook, добавить в `hooks/index.ts` директиву `'use client'`.
12. Экспортировать наружу только нужные generated-типы, DTO, `*HttpClient`, `*RestApi` и `hooks` через корневой `index.ts`.
Что агент не должен делать:
- Не править файлы в `generated/` руками.
- Не импортировать операции и `HttpClient` из `generated/` или SDK в UI/components.
- Не импортировать `operationsTree` или полный `*RestApi` в GET-хуки.
- Не добавлять GET-хуки для POST, PUT, PATCH, DELETE.
- Не добавлять бизнес-флаги, тосты, редиректы и UI-состояние в GET-хук.
- Не создавать словари enum-маппинга внутри GET-хука.
- Не объявлять DTO и response-типы в файле хука.
- Не вызывать `useSWR` условно.
- Не добавлять `throw` в fetcher для неготовых params.
## `client.ts`
`client.ts` содержит только настроенный транспортный `HttpClient`.
```ts
// src/infra/pet-store-rest-api/client.ts
import { HttpClient } from './generated'
export const petStoreHttpClient = new HttpClient({
baseUrl: 'https://example.com/api',
})
```
`client.ts` не содержит расширения типов, `declare module`, `Extended`-типы, GET-хуки, `operationsTree`, `createApiClient` и бизнес-логику.
Авторизация, refresh token, логирование и другие настройки транспорта задаются опциями и хуками `HttpClient` в этом же файле: [Кастомизация HTTP-клиента](./http-client.md).
## `rest-api.ts`
`rest-api.ts` собирает полный bound API-клиент из всего generated дерева операций.
```ts
// src/infra/pet-store-rest-api/rest-api.ts
import { createApiClient, operationsTree } from './generated'
import { petStoreHttpClient } from './client'
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
```
Импорт `operationsTree` означает полный клиент: в bound API доступны все операции API.
Если generated-код вынесен в SDK-пакет, используйте subpath exports:
```ts
// src/infra/pet-store-rest-api/rest-api.ts
import { createApiClient } from '@company/pet-store-rest-api-sdk/create-api-client'
import { operationsTree } from '@company/pet-store-rest-api-sdk/operations-tree'
import { petStoreHttpClient } from './client'
export const petStoreRestApi = createApiClient(petStoreHttpClient, operationsTree)
```
## GET-хуки
GET-хуки пишутся вручную после проверки generated-операций.
Пример для операции `getPetById`:
```ts
// src/infra/pet-store-rest-api/hooks/use-get-pet-detail.hook.ts
import { getPetById } from '../generated/operations/get-pet-by-id'
import type { SWRConfiguration } from 'swr'
import useSWR from 'swr'
import { petStoreHttpClient } from '../client'
import type { GetPetByIdParams, Pet } from '../generated'
export const getPetDetailKey = (params?: GetPetByIdParams | null) => {
if (!params?.petId) {
return null
}
return ['pet-store-rest-api', `/pet/${params.petId}`] as const
}
/**
* Получает детальную карточку питомца с кешированием результата.
*/
export const useGetPetDetail = (
params?: GetPetByIdParams | null,
config?: SWRConfiguration<Pet>,
) => {
const key = getPetDetailKey(params)
const fetcher = () => getPetById(petStoreHttpClient, params as GetPetByIdParams)
return useSWR<Pet>(key, fetcher, config)
}
```
Типы импортируются как `import type` из `./generated`: generated `index.ts` реэкспортирует все типы из `data-contracts.ts`.
Подробный контракт key-функций, `params`, `config` и запретов описан в разделе [GET-хуки REST-клиента](../../SKILL.md#get-хуки-rest-клиента).
## Расширение сгенерированных типов
Файлы в `generated/` не правятся руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (`object`, `unknown`, отсутствующее поле), расширения живут в `types/`.
```text
src/infra/biocad-less-rest-api/
├── generated/
│ ├── data-contracts.ts
│ ├── operations/
│ └── index.ts
├── types/
│ ├── term.ts
│ └── index.ts
├── client.ts
├── rest-api.ts
└── index.ts
```
Пример расширения generated-типа:
```ts
// src/infra/biocad-less-rest-api/types/term.ts
import type { TermRecordItem } from '../generated/data-contracts'
declare module '../generated/data-contracts' {
interface TermRecordItem {
media?: {
file?: string
title?: string
url?: string
}
}
}
export type TermRecordItemExtended = Omit<
TermRecordItem,
'categories' | 'tags' | 'fields'
> & {
categories?: Array<{
_id?: string
id?: string
slug?: string
name?: string
}>
tags?: Array<{
_id?: string
id?: string
slug?: string
name?: string
}>
fields?: Record<string, unknown>
}
```
```ts
// src/infra/biocad-less-rest-api/types/index.ts
export type { TermRecordItemExtended } from './term'
```
`declare module` нацеливается на `../generated/data-contracts` — именно там живут generated-интерфейсы. Он используется для добавления отсутствующих полей. `Extended`-тип используется, когда нужно переопределить неточные поля, не трогая generated-файлы.
## Публичный API
```ts
// src/infra/pet-store-rest-api/index.ts
export { petStoreHttpClient } from './client'
export { petStoreRestApi } from './rest-api'
export type {
FindPetsByStatusParams,
GetPetByIdParams,
Pet,
} from './generated'
export * from './hooks'
```
Наружу импортируют только из `infra/pet-store-rest-api`, не из `generated/`.
Если у модуля есть расширенные типы, они тоже реэкспортируются через `index.ts`:
```ts
// src/infra/biocad-less-rest-api/index.ts
export type { TermRecordItemExtended } from './types'
```
## Регенерация
При изменении OpenAPI-схемы:
```bash
npm run codegen:pet-store-rest-api
```
Что меняется:
- Папка `generated/` — перезаписывается генератором целиком.
- `client.ts`, `rest-api.ts`, `hooks/`, `types/`, `index.ts` — не трогаются автоматически.
Если после регенерации поменялись имена операций, сигнатуры или типы, это исправляется в ручном коде модуля: `rest-api.ts`, GET-хуки, minimal clients и реэкспорты.
## Следующий шаг
После генерации настройте транспорт в `client.ts` по разделу [Кастомизация HTTP-клиента](./http-client.md), соберите полный API в `rest-api.ts`, затем проверьте [использование REST-клиента](../../SKILL.md#использование-rest-клиента) или добавьте [GET-хук REST-клиента](../../SKILL.md#get-хуки-rest-клиента) для Client Components.