mirror of
https://github.com/gromlab-ru/slm-design.git
synced 2026-08-22 15:30:16 +03:00
343 lines
15 KiB
Markdown
343 lines
15 KiB
Markdown
---
|
||
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.
|