RTK Query Codegen — механизм автоматической генерации API-клиентов, типов и endpoint-описаний на основе OpenAPI-схемы. Инструмент позволяет отказаться от ручного создания большого количества запросов и существенно уменьшает дублирование кода.
Основная идея codegen заключается в следующем:
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 работает по следующей схеме:
Загружается OpenAPI JSON/YAML.
Анализируются:
Формируются:
Генерируется готовый файл 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
Генератор обычно не создает 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: () => ({}),
})
Такой подход позволяет:
Создается файл:
// 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,
})
Путь до OpenAPI-схемы.
schemaFile: './schema.json'
Поддерживаются:
Пример URL:
schemaFile: 'https://api.example.com/openapi.json'
Файл с базовым API.
apiFile: './src/services/emptyApi.ts'
Имя экспортируемого API.
apiImport: 'emptyApi'
Файл генерации.
outputFile: './src/services/generatedApi.ts'
Имя итогового API.
exportName: 'generatedApi'
Автоматическая генерация 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`,
}),
}),
}),
})
При включенном hooks: true создаются:
export const {
useGetUsersQuery,
useCreateUserMutation,
} = injectedRtkApi
Использование:
const { data, isLoading } = useGetUsersQuery()
Mutation:
const [createUser] = useCreateUserMutation()
Codegen автоматически формирует:
export type User = {
id: number
name: string
}
Типы аргументов:
export type GetUsersApiArg = void
Типы ответа:
export type GetUsersApiResponse = User[]
Типы body:
export type CreateUserApiArg = {
body: CreateUserRequest
}
Ключевую роль играет operationId.
Пример:
/users:
get:
operationId: getUsers
Без operationId генерация может создавать неудобные имена:
useUsersQuery
useUsers2Query
Корректно заданный operationId обеспечивает:
Методы автоматически сопоставляются:
| HTTP Method | RTK Query Type |
|---|---|
| GET | query |
| POST | mutation |
| PUT | mutation |
| PATCH | mutation |
| DELETE | mutation |
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
}
OpenAPI:
/users/{id}:
get:
operationId: getUser
Generated:
query: (queryArg) => ({
url: `/users/${queryArg.id}`,
})
Тип:
export type GetUserApiArg = {
id: number
}
OpenAPI:
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserDto'
Generated:
query: (queryArg) => ({
url: `/users`,
method: 'POST',
body: queryArg.createUserDto,
})
Codegen учитывает:
Пример:
responses:
200:
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/User'
Generated:
type GetUsersApiResponse = User[]
OpenAPI:
status:
type: string
enum:
- active
- blocked
Generated:
status?: 'active' | 'blocked'
OpenAPI:
email:
type: string
nullable: true
Generated:
email?: string | null
Codegen умеет обрабатывать сложные композиции схем.
allOf:
- $ref: '#/components/schemas/BaseUser'
- type: object
Generated:
type User = BaseUser & {
...
}
oneOf:
- $ref: '#/components/schemas/Cat'
- $ref: '#/components/schemas/Dog'
Generated:
type Animal = Cat | Dog
Можно автоматически добавлять tagTypes:
tag: true
Generated:
providesTags: ['User']
invalidatesTags: ['User']
Это особенно полезно для автоматической инвалидации кэша.
Codegen поддерживает кастомизацию endpoint-ов.
Пример:
endpointOverrides: [
{
pattern: 'getUsers',
override: {
providesTags: ['User'],
},
},
]
Generated endpoint будет дополнен:
providesTags: ['User']
Иногда backend возвращает неудобную структуру:
{
"data": [...]
}
Можно переопределить endpoint:
endpointOverrides: [
{
pattern: 'getUsers',
override: {
transformResponse: (response) => response.data,
},
},
]
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.
Крупные backend API могут генерироваться в разные файлы.
Пример:
generated/
├── usersApi.ts
├── postsApi.ts
├── commentsApi.ts
Отдельные config-файлы:
defineConfig({
schemaFile: './schema.json',
outputFile: './generated/usersApi.ts',
filterEndpoints: ['getUsers'],
})
Позволяет генерировать только часть API.
filterEndpoints: [
'getUsers',
'createUser',
]
Это особенно полезно:
Позволяет генерировать enum вместо string union.
useEnumType: true
Generated:
export enum UserStatus {
Active = 'active',
Blocked = 'blocked',
}
По умолчанию аргументы:
queryArg.body
queryArg.id
С flattenArg:
flattenArg: true
Получается:
query: ({ id, body }) => ({
url: `/users/${id}`,
body,
})
Позволяет более явно указывать optional значения.
unionUndefined: true
Generated:
name: string | undefined
Swagger 2.0 также поддерживается.
schemaFile: './swagger.json'
Codegen автоматически конвертирует спецификацию.
Пример для Vite:
{
"scripts": {
"predev": "npm run generate-api",
"prebuild": "npm run generate-api"
}
}
Теперь генерация выполняется автоматически.
Иногда используется отдельный watcher:
nodemon --watch schema.json --exec npm run generate-api
Generated-файлы обычно исключаются:
{
"ignorePatterns": [
"src/services/generatedApi.ts"
]
}
Часто generated-файлы форматируются автоматически:
npm run generate-api && prettier --write .
Некоторые OpenAPI-схемы содержат циклические зависимости:
User:
properties:
manager:
$ref: '#/components/schemas/User'
Такие схемы иногда вызывают:
Большие enterprise OpenAPI-файлы могут содержать:
Последствия:
users-schema.json
posts-schema.json
billing-schema.json
filterEndpoints
Если hooks не нужны:
hooks: false
Иногда generated code компилируется отдельным tsconfig.
Backend может возвращать данные, не соответствующие OpenAPI.
Например:
id:
type: integer
Но реально приходит:
{
"id": "123"
}
TypeScript не сможет защитить runtime.
Для дополнительной безопасности используют:
Пример:
const UserSchema = z.object({
id: z.number(),
})
Generated API можно расширять вручную.
export const extendedApi = generatedApi.injectEndpoints({
endpoints: (build) => ({
customEndpoint: build.query({
query: () => '/custom',
}),
}),
})
Можно enhanceEndpoints:
export const enhancedApi = generatedApi.enhanceEndpoints({
addTagTypes: ['User'],
endpoints: {
getUsers: {
providesTags: ['User'],
},
},
})
Generated API поддерживает lazy injection.
emptyApi.injectEndpoints({
endpoints: ...
})
Это уменьшает initial bundle size.
RTK Query Codegen полностью совместим с SSR:
Generated endpoints являются обычными RTK Query endpoints.
Пример server-side prefetch:
store.dispatch(
generatedApi.endpoints.getUsers.initiate()
)
Ожидание завершения:
await Promise.all(
store.dispatch(
generatedApi.util.getRunningQueriesThunk()
)
)
Часто используются разные generated API:
generated/
├── v1Api.ts
├── v2Api.ts
В monorepo generated code часто выносится:
packages/
├── api-client/
├── frontend/
└── backend/
Frontend импортирует готовый generated client:
import { generatedApi } from '@company/api-client'
Типичный pipeline:
1. Получение OpenAPI
2. Генерация RTK Query
3. TypeScript check
4. Build
5. Deploy
В CI часто сравниваются generated-файлы:
git diff
Если backend изменил OpenAPI — frontend автоматически обнаружит изменения.
Вместо сотен endpoint-описаний генерируется один файл.
Frontend и backend используют одинаковую схему.
Практически отсутствует ручное описание типов.
Новые endpoints появляются автоматически после обновления OpenAPI.
Исключаются:
Плохая схема приводит к плохому generated-коду.
Enterprise API могут генерировать десятки тысяч строк.
Некоторые сложные сценарии требуют manual override.
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'],
},
},
],
})