Skip to content

Автоматическая генерация

Если у API есть OpenAPI-спецификация — клиент генерируется утилитой @gromlab/api-codegen (обёртка над swagger-typescript-api). Ручной код для таких API не пишется.

Когда схемы нет — Ручная генерация.

В примерах ниже используется условный API pet-project-api (kebab-case в путях) / petProjectApi (camelCase в коде). В реальном проекте имена выбираются по конкретному API.

Установка

bash
npm install -D @gromlab/api-codegen

Скрипт генерации в package.json — по одному на каждый API:

json
{
  "scripts": {
    "codegen:pet-project-api": "api-codegen --config src/infrastructure/pet-project-api/config/pet-project-api.config.ts"
  }
}

Конфиг и опции — в репозитории @gromlab/api-codegen.

Структура модуля

Клиент кладётся в слой infrastructure/ отдельным модулем по имени API (kebab-case):

text
src/infrastructure/
└── pet-project-api/
    ├── generated/                          # сегмент сгенерированного кода
    │   └── pet-project-api.generated.ts    # сгенерировано — не править
    ├── types/                              # расширения сгенерированных типов
    │   ├── user.ts                         # declare module + Extended-тип
    │   └── index.ts                        # реэкспорт расширений
    ├── hooks/                              # SWR-хуки для клиентских компонентов
    │   ├── use-user-list.hook.ts
    │   ├── use-user-detail.hook.ts
    │   └── index.ts                        # реэкспорт хуков
    ├── config/                             # конфиги модуля
    │   └── pet-project-api.config.ts        # конфиг генерации клиента
    ├── client.ts                           # настройка HttpClient, инстанс Api
    └── index.ts                            # публичный API модуля
ФайлРольКто правит
generated/{service-name}.generated.tsСгенерированный код: типы, class Api, class HttpClientcodegen, не править
types/{сущность}.tsdeclare module + Extended-типы по сущностиразработчик
types/index.tsРеэкспорт публичных расширенийразработчик
hooks/use-{action}.hook.tsSWR-хук поверх метода клиентаразработчик
hooks/index.tsРеэкспорт хуковразработчик
config/{service-name}.config.tsПараметры генерации для конкретного APIразработчик
client.tsbaseUrl из env, конфиг HttpClient, инстанс new Api(...)разработчик
index.tsПубличный API: инстанс сервиса, расширенные типы, хукиразработчик

client.ts и index.ts — единственные корневые файлы модуля. Все остальные файлы живут в сегментах (generated/, types/, hooks/, config/).

Имя сгенерированного файла — {service-name}.generated.ts (имя сервиса в kebab-case + суффикс .generated.ts). Суффикс сигнализирует «не править руками».

client.ts

Тонкий ручной слой поверх сгенерированного кода. Делает три вещи: читает и нормализует baseUrl, конфигурирует HttpClient, создаёт именованный инстанс сервиса.

ts
// src/infrastructure/pet-project-api/client.ts
import { Api, HttpClient } from './generated/pet-project-api.generated'

const resolvedBaseUrl = process.env.NEXT_PUBLIC_API_URL
  .replace(/\/+$/, '')   // убираем хвостовой слэш
  .replace(/\/v1$/, '')  // версия уже в путях методов — режем дубль

const httpClient = new HttpClient({
  baseApiParams: {
    secure: false,
    headers: {
      'Content-Type': 'application/json',
      // кастомные заголовки API — если требуются
      // 'X-App-Key': '...',
    },
  },
})

httpClient.baseUrl = resolvedBaseUrl

export const petProjectApi = new Api(httpClient)

Имя инстанса = имя сервиса

Инстанс называется по имени API в camelCase, не унифицированно api/client. Это даёт процедурное обращение и однозначность при работе с несколькими сервисами:

ts
import { petProjectApi } from 'infrastructure/pet-project-api'

const user = await petProjectApi.user.getUser(id)

При нескольких API — каждый со своим именем:

ts
import { petProjectApi } from 'infrastructure/pet-project-api'
import { paymentsApi } from 'infrastructure/payments-api'

const user = await petProjectApi.user.list()
const invoice = await paymentsApi.invoices.list()

Нормализация baseUrl

@gromlab/api-codegen может включать версию (/v1) в baseUrl сгенерированного кода, а пути методов уже содержат её — отсюда дубль. Стандартный приём:

ts
.replace(/\/+$/, '')   // хвостовой слэш
.replace(/\/v1$/, '')  // версия (если фигурирует в путях)

Подгоняется под конкретный API: если версия в путях не повторяется — второй replace не нужен.

Расширения типов

Автогенерация не покрывает все реальные поля API: иногда тип object, иногда поле просто отсутствует. Расширения живут в types/, по файлу на сущность.

Две техники:

declare module — добавление полей

Дополняет существующий интерфейс из generated.ts. Сама сгенерированная декларация не трогается.

ts
// src/infrastructure/pet-project-api/types/user.ts
import type { User } from '../generated/pet-project-api.generated'

declare module '../generated/pet-project-api.generated' {
  interface User {
    avatar?: {
      file?: string
      title?: string
      url?: string
    }
  }
}

Extended через Omit & {...} — переопределение полей

Когда автогенерация даёт object или общий тип, а реально структура известна — создаётся отдельный тип UserExtended (по имени сущности + суффикс Extended).

ts
// src/infrastructure/pet-project-api/types/user.ts
export type UserExtended = Omit<User, 'roles' | 'tags' | 'fields'> & {
  roles?: 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/infrastructure/pet-project-api/types/index.ts
export type { UserExtended } from './user'

Правила

  • Расширения — только в types/, не в client.ts и не в сгенерированном файле.
  • Один файл на сущность (имя файла — kebab-case по сущности: user.ts, order.ts, invoice.ts).
  • При регенерации generated/{service-name}.generated.ts файлы в types/ не затрагиваются.
  • Если сломался Extended-тип после regen — синхронизировать руками.

Хуки для клиентских компонентов

В клиентских компонентах вызовы клиента не делаются напрямую — компонент получает готовый хук, который инкапсулирует SWR + метод клиента. Хуки живут в сегменте hooks/, по файлу на операцию.

ts
// src/infrastructure/pet-project-api/hooks/use-user-list.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petProjectApi } from '../client'
import type { User } from '../generated/pet-project-api.generated'

/**
 * Получение списка пользователей.
 */
export const useUserList = (
  query?: { limit?: number; offset?: number },
  config?: SWRConfiguration,
) => {
  return useSWR<User[]>(
    ['pet-project-api', 'user', 'list', query],
    () => petProjectApi.user.list(query ?? {}),
    config,
  )
}
ts
// src/infrastructure/pet-project-api/hooks/use-user-detail.hook.ts
import useSWR from 'swr'
import type { SWRConfiguration } from 'swr'
import { petProjectApi } from '../client'
import type { UserExtended } from '../types'

/**
 * Получение пользователя по идентификатору.
 */
export const useUserDetail = (
  id: string | null,
  config?: SWRConfiguration,
) => {
  const key = id ? ['pet-project-api', 'user', 'detail', id] : null
  const fetcher = () => petProjectApi.user.getUser(id!) as Promise<UserExtended>

  return useSWR<UserExtended>(key, fetcher, config)
}
ts
// src/infrastructure/pet-project-api/hooks/index.ts
export { useUserList } from './use-user-list.hook'
export { useUserDetail } from './use-user-detail.hook'

Правила хуков

  • Один файл — один хук, имя файла use-{action}.hook.ts (Именование).
  • Тонкая обёртка над SWR. Внутри — построение ключа, fetcher через метод клиента, возврат useSWR(...). Никакой бизнес-логики.
  • Ключ начинается с имени сервиса (['pet-project-api', ...]) — изолирует кеш между разными API.
  • Условный ключ для опциональных параметров: id ? [...key, id] : null. null приостанавливает запрос, пока параметры не готовы.
  • Параметр config?: SWRConfiguration — даёт потребителю переопределить ревалидацию, fallbackData, suspense и т.п. без обёрток.

Публичный API модуля

Из index.ts экспортируются инстанс, расширенные типы и хуки. Сырые типы из generated/ экспортируются по необходимости — точечно.

ts
// src/infrastructure/pet-project-api/index.ts
export { petProjectApi } from './client'
export type { UserExtended } from './types'
export * from './hooks'

Регенерация

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

bash
npm run codegen:pet-project-api

Что меняется:

  • generated/{service-name}.generated.ts — перезаписывается полностью, изменения коммитятся.
  • client.ts, types/, config/, index.tsне трогаются автоматически.

Поломка контракта (изменение типов в схеме) ловится TypeScript при сборке проекта. Если ломаются Extended-типы — синхронизировать вручную в соответствующих файлах types/.

Сгенерированный файл коммитится

Файл generated/{service-name}.generated.ts не добавляется в .gitignore — попадает в репозиторий вместе с остальным кодом.

Причины:

  • Детерминированная сборка. npm run build не зависит от доступности OpenAPI-схемы (обычно она на удалённом сервере). Сервис лёг — прод собирается.
  • Видимость изменений в PR. Diff показывает, что именно поменялось в контракте API между версиями.
  • Простой онбординг. После git clone IDE сразу видит типы, без предварительной генерации.
  • Фиксация версии контракта. Пересборка старого коммита даёт ровно тот клиент, что был тогда.

Регенерация — ручная команда при обновлении схемы, не хук predev/prebuild. Запускается осознанно.

Исключение возможно, только если OpenAPI-схема лежит в этом же репозитории и генерация быстрая, без сети — тогда допустимо добавить сегмент generated/ в .gitignore и хук prebuild, по аналогии со спрайтами. На практике встречается редко.