Files
2026-08-01 09:31:08 +03:00

15 KiB
Raw Permalink Blame History

title, description, keywords
title description keywords
Автогенерация REST-клиента Генерация split REST-клиента из OpenAPI-спецификации.
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-клиента.

Пример API

В примерах используется Swagger Petstore:

https://petstore3.swagger.io/api/v3/openapi.json

Имена модуля:

src/infra/pet-store-rest-api/
petStoreRestApi

Скрипт генерации

@gromlab/api-codegen не устанавливается в devDependencies. Используем npx @gromlab/api-codegen@latest, чтобы запускать свежую версию.

{
  "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, без бизнес-логики и композиции.

Генерация

npm run codegen:pet-store-rest-api

Ожидаемый результат:

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.tscreateApiClient, который привязывает дерево операций к HttpClient.
  • index.ts — входная точка generated-кода, реэкспортирует всё перечисленное.

Файлы в generated/ не правятся руками и коммитятся в репозиторий.

Enum-значения в split-режиме генерируются как union-типы строковых литералов. Отдельных runtime enum в generated/ нет: в коде используются строковые литералы вроде 'available', а тип проверяет их допустимость.

Проверка операций

После генерации откройте generated/operations-tree.ts и проверьте фактические имена operation-функций и структуру дерева.

Для Petstore нужны GET-операции вида:

import { findPetsByStatus } from './generated/operations/find-pets-by-status'
import { getPetById } from './generated/operations/get-pet-by-id'

Точечная operation вызывается через настроенный HttpClient:

getPetById(petStoreHttpClient, { petId: 10 })
findPetsByStatus(petStoreHttpClient, { status: 'available' })

После сборки полного клиента в rest-api.ts те же операции доступны как bound API:

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/:

import { getPetById } from '../generated/operations/get-pet-by-id'

Если generated-код вынесен в SDK-пакет, импортируйте operation через subpath export пакета:

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.

// 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-клиента.

rest-api.ts

rest-api.ts собирает полный bound API-клиент из всего generated дерева операций.

// 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:

// 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:

// 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-клиента.

Расширение сгенерированных типов

Файлы в generated/ не правятся руками. Если OpenAPI-спецификация неполная или генератор дал слишком общий тип (object, unknown, отсутствующее поле), расширения живут в types/.

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-типа:

// 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>
}
// src/infra/biocad-less-rest-api/types/index.ts
export type { TermRecordItemExtended } from './term'

declare module нацеливается на ../generated/data-contracts — именно там живут generated-интерфейсы. Он используется для добавления отсутствующих полей. Extended-тип используется, когда нужно переопределить неточные поля, не трогая generated-файлы.

Публичный API

// 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:

// src/infra/biocad-less-rest-api/index.ts
export type { TermRecordItemExtended } from './types'

Регенерация

При изменении OpenAPI-схемы:

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-клиента, соберите полный API в rest-api.ts, затем проверьте использование REST-клиента или добавьте GET-хук REST-клиента для Client Components.