RTK Query Codegen

RTK Query Codegen — механизм автоматической генерации API-клиентов, типов и endpoint-описаний на основе OpenAPI-схемы. Инструмент позволяет отказаться от ручного создания большого количества запросов и существенно уменьшает дублирование кода.

Основная идея codegen заключается в следующем:

  • OpenAPI-документ становится единственным источником правды;
  • endpoints генерируются автоматически;
  • типы запросов и ответов создаются без ручного описания;
  • уменьшается вероятность рассинхронизации frontend и backend;
  • API становится легче поддерживать при постоянных изменениях.

RTK Query предоставляет официальный пакет генерации:

@rtk-query/codegen-openapi

Установка генератора

Установка выполняется через npm:

npm install @rtk-query/codegen-openapi --save-dev

Либо через yarn:

yarn add @rtk-query/codegen-openapi -D

После установки становится доступна CLI-команда:

npx @rtk-query/codegen-openapi

Архитектура генерации

Codegen работает по следующей схеме:

  1. Загружается OpenAPI JSON/YAML.

  2. Анализируются:

    • paths;
    • methods;
    • schemas;
    • параметры;
    • requestBody;
    • responses.
  3. Формируются:

    • TypeScript-типы;
    • endpoints RTK Query;
    • hooks React;
    • базовые query/mutation-конструкции.
  4. Генерируется готовый файл API.

Типичный pipeline:

OpenAPI -> Codegen -> RTK Query API -> React Hooks

Структура проекта

Пример структуры:

src/
├── app/
│   └── store.ts
├── services/
│   ├── emptyApi.ts
│   ├── generatedApi.ts
│   └── openapi/
│       └── schema.json
├── components/
└── pages/

codegen.ts

Создание базового emptyApi

Генератор обычно не создает API с нуля, а расширяет существующий emptyApi.

// services/emptyApi.ts

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

export const emptyApi = createApi({
    reducerPath: 'api',
    baseQuery: fetchBaseQuery({
        baseUrl: 'https://api.example.com',
    }),
    endpoints: () => ({}),
})

Такой подход позволяет:

  • централизованно хранить baseQuery;
  • подключать авторизацию;
  • использовать prepareHeaders;
  • расширять API частями;
  • выполнять code splitting.

Конфигурация codegen

Создается файл:

// codegen.ts

import { defineConfig } from '@rtk-query/codegen-openapi'

export default defineConfig({
    schemaFile: './src/services/openapi/schema.json',

    apiFile: './src/services/emptyApi.ts',

    apiImport: 'emptyApi',

    outputFile: './src/services/generatedApi.ts',

    exportName: 'generatedApi',

    hooks: true,
})

Основные параметры конфигурации

schemaFile

Путь до OpenAPI-схемы.

schemaFile: './schema.json'

Поддерживаются:

  • локальные JSON;
  • локальные YAML;
  • remote URL.

Пример URL:

schemaFile: 'https://api.example.com/openapi.json'

apiFile

Файл с базовым API.

apiFile: './src/services/emptyApi.ts'

apiImport

Имя экспортируемого API.

apiImport: 'emptyApi'

outputFile

Файл генерации.

outputFile: './src/services/generatedApi.ts'

exportName

Имя итогового API.

exportName: 'generatedApi'

hooks

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

hooks: true

После генерации появятся:

useGetUsersQuery
useCreatePostMutation
useDeleteCommentMutation

Запуск генерации

Ручной запуск:

npx @rtk-query/codegen-openapi codegen.ts

Через package.json:

{
  "scripts": {
    "generate-api": "rtk-query-codegen-openapi codegen.ts"
  }
}

Запуск:

npm run generate-api

Генерируемый код

На основе OpenAPI generator создает полноценные endpoints.

Например, OpenAPI:

/users:
  get:
    operationId: getUsers

Превращается в:

const injectedRtkApi = emptyApi.injectEndpoints({
    endpoints: (build) => ({
        getUsers: build.query<GetUsersApiResponse, GetUsersApiArg>({
            query: () => ({
                url: `/users`,
            }),
        }),
    }),
})

Генерация React Hooks

При включенном hooks: true создаются:

export const {
    useGetUsersQuery,
    useCreateUserMutation,
} = injectedRtkApi

Использование:

const { data, isLoading } = useGetUsersQuery()

Mutation:

const [createUser] = useCreateUserMutation()

Генерация TypeScript-типов

Codegen автоматически формирует:

export type User = {
    id: number
    name: string
}

Типы аргументов:

export type GetUsersApiArg = void

Типы ответа:

export type GetUsersApiResponse = User[]

Типы body:

export type CreateUserApiArg = {
    body: CreateUserRequest
}

Работа с operationId

Ключевую роль играет operationId.

Пример:

/users:
  get:
    operationId: getUsers

Без operationId генерация может создавать неудобные имена:

useUsersQuery
useUsers2Query

Корректно заданный operationId обеспечивает:

  • стабильные имена;
  • предсказуемость API;
  • читаемость generated-кода.

Генерация query и mutation

Методы автоматически сопоставляются:

HTTP Method RTK Query Type
GET query
POST mutation
PUT mutation
PATCH mutation
DELETE mutation

Генерация query parameters

OpenAPI:

/users:
  get:
    parameters:
      - name: page
        in: query
        schema:
          type: integer

Generated:

getUsers: build.query<GetUsersApiResponse, GetUsersApiArg>({
    query: (queryArg) => ({
        url: `/users`,
        params: {
            page: queryArg.page,
        },
    }),
})

Тип:

export type GetUsersApiArg = {
    page?: number
}

Path parameters

OpenAPI:

/users/{id}:
  get:
    operationId: getUser

Generated:

query: (queryArg) => ({
    url: `/users/${queryArg.id}`,
})

Тип:

export type GetUserApiArg = {
    id: number
}

Request Body

OpenAPI:

requestBody:
  content:
    application/json:
      schema:
        $ref: '#/components/schemas/CreateUserDto'

Generated:

query: (queryArg) => ({
    url: `/users`,
    method: 'POST',
    body: queryArg.createUserDto,
})

Response typing

Codegen учитывает:

  • status codes;
  • content types;
  • schema references;
  • arrays;
  • unions;
  • nullable.

Пример:

responses:
  200:
    content:
      application/json:
        schema:
          type: array
          items:
            $ref: '#/components/schemas/User'

Generated:

type GetUsersApiResponse = User[]

enum в OpenAPI

OpenAPI:

status:
  type: string
  enum:
    - active
    - blocked

Generated:

status?: 'active' | 'blocked'

nullable поля

OpenAPI:

email:
  type: string
  nullable: true

Generated:

email?: string | null

allOf, oneOf, anyOf

Codegen умеет обрабатывать сложные композиции схем.

allOf

allOf:
  - $ref: '#/components/schemas/BaseUser'
  - type: object

Generated:

type User = BaseUser & {
    ...
}

oneOf

oneOf:
  - $ref: '#/components/schemas/Cat'
  - $ref: '#/components/schemas/Dog'

Generated:

type Animal = Cat | Dog

Генерация tagTypes

Можно автоматически добавлять tagTypes:

tag: true

Generated:

providesTags: ['User']
invalidatesTags: ['User']

Это особенно полезно для автоматической инвалидации кэша.


Настройка override

Codegen поддерживает кастомизацию endpoint-ов.

Пример:

endpointOverrides: [
    {
        pattern: 'getUsers',
        override: {
            providesTags: ['User'],
        },
    },
]

Generated endpoint будет дополнен:

providesTags: ['User']

transformResponse

Иногда backend возвращает неудобную структуру:

{
  "data": [...]
}

Можно переопределить endpoint:

endpointOverrides: [
    {
        pattern: 'getUsers',
        override: {
            transformResponse: (response) => response.data,
        },
    },
]

Настройка baseQuery

Codegen не ограничивает baseQuery.

Можно использовать:

fetchBaseQuery

или собственную реализацию:

const customBaseQuery = async (
    args,
    api,
    extraOptions
) => {
    ...
}

Авторизация

Типичный пример:

baseQuery: fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers, { getState }) => {
        const token = getState().auth.token

        if (token) {
            headers.set('authorization', `Bearer ${token}`)
        }

        return headers
    },
})

Все generated endpoints автоматически используют этот baseQuery.


Разделение API по модулям

Крупные backend API могут генерироваться в разные файлы.

Пример:

generated/
├── usersApi.ts
├── postsApi.ts
├── commentsApi.ts

Отдельные config-файлы:

defineConfig({
    schemaFile: './schema.json',
    outputFile: './generated/usersApi.ts',
    filterEndpoints: ['getUsers'],
})

filterEndpoints

Позволяет генерировать только часть API.

filterEndpoints: [
    'getUsers',
    'createUser',
]

Это особенно полезно:

  • при микрофронтендах;
  • модульной архитектуре;
  • постепенной миграции;
  • уменьшении размера bundle.

useEnumType

Позволяет генерировать enum вместо string union.

useEnumType: true

Generated:

export enum UserStatus {
    Active = 'active',
    Blocked = 'blocked',
}

flattenArg

По умолчанию аргументы:

queryArg.body
queryArg.id

С flattenArg:

flattenArg: true

Получается:

query: ({ id, body }) => ({
    url: `/users/${id}`,
    body,
})

unionUndefined

Позволяет более явно указывать optional значения.

unionUndefined: true

Generated:

name: string | undefined

Генерация из Swagger

Swagger 2.0 также поддерживается.

schemaFile: './swagger.json'

Codegen автоматически конвертирует спецификацию.


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

Пример для Vite:

{
  "scripts": {
    "predev": "npm run generate-api",
    "prebuild": "npm run generate-api"
  }
}

Теперь генерация выполняется автоматически.


Watch mode

Иногда используется отдельный watcher:

nodemon --watch schema.json --exec npm run generate-api

Generated code и ESLint

Generated-файлы обычно исключаются:

{
  "ignorePatterns": [
    "src/services/generatedApi.ts"
  ]
}

Generated code и Prettier

Часто generated-файлы форматируются автоматически:

npm run generate-api && prettier --write .

Проблемы circular references

Некоторые OpenAPI-схемы содержат циклические зависимости:

User:
  properties:
    manager:
      $ref: '#/components/schemas/User'

Такие схемы иногда вызывают:

  • слишком глубокие типы;
  • recursion limits TypeScript;
  • медленную компиляцию.

Проблемы large schema

Большие enterprise OpenAPI-файлы могут содержать:

  • тысячи endpoints;
  • десятки тысяч типов;
  • огромные union-типы.

Последствия:

  • рост времени TypeScript compilation;
  • высокая нагрузка на IDE;
  • увеличение памяти tsserver.

Оптимизация больших схем

Разделение API

users-schema.json
posts-schema.json
billing-schema.json

Модульная генерация

filterEndpoints

Отключение hooks

Если hooks не нужны:

hooks: false

Отдельный tsconfig

Иногда generated code компилируется отдельным tsconfig.


Проблемы inconsistent backend schemas

Backend может возвращать данные, не соответствующие OpenAPI.

Например:

id:
  type: integer

Но реально приходит:

{
  "id": "123"
}

TypeScript не сможет защитить runtime.


Runtime validation

Для дополнительной безопасности используют:

  • zod;
  • valibot;
  • io-ts;
  • superstruct.

Пример:

const UserSchema = z.object({
    id: z.number(),
})

Комбинация codegen и ручных endpoint-ов

Generated API можно расширять вручную.

export const extendedApi = generatedApi.injectEndpoints({
    endpoints: (build) => ({
        customEndpoint: build.query({
            query: () => '/custom',
        }),
    }),
})

Переопределение generated endpoint

Можно enhanceEndpoints:

export const enhancedApi = generatedApi.enhanceEndpoints({
    addTagTypes: ['User'],
    endpoints: {
        getUsers: {
            providesTags: ['User'],
        },
    },
})

Code splitting

Generated API поддерживает lazy injection.

emptyApi.injectEndpoints({
    endpoints: ...
})

Это уменьшает initial bundle size.


SSR и generated API

RTK Query Codegen полностью совместим с SSR:

  • Next.js;
  • Remix;
  • custom SSR.

Generated endpoints являются обычными RTK Query endpoints.


Использование с Next.js

Пример server-side prefetch:

store.dispatch(
    generatedApi.endpoints.getUsers.initiate()
)

Ожидание завершения:

await Promise.all(
    store.dispatch(
        generatedApi.util.getRunningQueriesThunk()
    )
)

Versioning API

Часто используются разные generated API:

generated/
├── v1Api.ts
├── v2Api.ts

Интеграция с monorepo

В monorepo generated code часто выносится:

packages/
├── api-client/
├── frontend/
└── backend/

Frontend импортирует готовый generated client:

import { generatedApi } from '@company/api-client'

CI/CD генерация

Типичный pipeline:

1. Получение OpenAPI
2. Генерация RTK Query
3. TypeScript check
4. Build
5. Deploy

Проверка изменений схемы

В CI часто сравниваются generated-файлы:

git diff

Если backend изменил OpenAPI — frontend автоматически обнаружит изменения.


Преимущества RTK Query Codegen

Снижение количества ручного кода

Вместо сотен endpoint-описаний генерируется один файл.


Единый контракт

Frontend и backend используют одинаковую схему.


Автоматическая типизация

Практически отсутствует ручное описание типов.


Ускорение разработки

Новые endpoints появляются автоматически после обновления OpenAPI.


Снижение количества ошибок

Исключаются:

  • опечатки URL;
  • неправильные типы;
  • ошибки body;
  • ошибки query params.

Недостатки RTK Query Codegen

Зависимость от качества OpenAPI

Плохая схема приводит к плохому generated-коду.


Огромные generated-файлы

Enterprise API могут генерировать десятки тысяч строк.


Сложность кастомизации

Некоторые сложные сценарии требуют manual override.


Проблемы backend consistency

Runtime может не совпадать с документацией.


Практический пример полной конфигурации

import { defineConfig } from '@rtk-query/codegen-openapi'

export default defineConfig({
    schemaFile: './openapi/schema.json',

    apiFile: './src/services/emptyApi.ts',

    apiImport: 'emptyApi',

    outputFile: './src/services/generatedApi.ts',

    exportName: 'generatedApi',

    hooks: true,

    tag: true,

    useEnumType: true,

    flattenArg: true,

    filterEndpoints: [
        'getUsers',
        'createUser',
        'deleteUser',
    ],

    endpointOverrides: [
        {
            pattern: 'getUsers',
            override: {
                providesTags: ['User'],
            },
        },
    ],
})