Интеграция RTK Query с OpenAPI позволяет автоматически генерировать API-слой на основе спецификации сервера. Такой подход устраняет дублирование кода между backend и frontend, уменьшает количество ошибок при изменении контрактов API и значительно ускоряет разработку.
OpenAPI-спецификация описывает:
RTK Query способен использовать эту спецификацию для автоматического создания:
При ручном создании API возникают типичные проблемы:
Backend изменил поле:
{
"username": "admin"
}
Frontend продолжает ожидать:
{
"login": "admin"
}
В результате:
Backend:
User:
type: object
properties:
id:
type: integer
email:
type: string
Frontend:
interface User {
id: number;
email: string;
}
Каждое изменение требует ручного обновления.
При большом API приходится вручную создавать:
OpenAPI-генерация устраняет большую часть этой рутины.
Для генерации RTK Query API используется пакет:
@rtk-query/codegen-openapi
Он анализирует OpenAPI-спецификацию и создает готовый API-модуль.
npm install @reduxjs/toolkit
npm install @rtk-query/codegen-openapi
Либо:
yarn add @reduxjs/toolkit
yarn add @rtk-query/codegen-openapi
Генератор поддерживает:
Пример YAML-спецификации:
openapi: 3.0.0
info:
title: Users API
version: 1.0.0
paths:
/users:
get:
operationId: getUsers
responses:
"200":
description: Users list
content:
application/json:
schema:
type: array
items:
$ref: "#/components/schemas/User"
components:
schemas:
User:
type: object
properties:
id:
type: integer
name:
type: string
Создается файл:
openapi-config.ts
import { defineConfig } fr om "@rtk-query/codegen-openapi";
export default defineConfig({
schemaFile: "http://localhost:3000/openapi.json",
apiFile: "./src/store/api/baseApi.ts",
apiImport: "baseApi",
outputFile: "./src/store/api/generatedApi.ts",
exportName: "generatedApi",
hooks: true,
});
Источник OpenAPI-спефикации.
Возможные варианты:
schemaFile: "./openapi.yaml"
или:
schemaFile: "https://api.site.com/openapi.json"
Файл, содержащий базовый RTK Query API.
Пример:
import { createApi, fetchBaseQuery } fr om "@reduxjs/toolkit/query/react";
export const baseApi = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "/api",
}),
endpoints: () => ({}),
});
Имя экспортируемого API.
apiImport: "baseApi"
соответствует:
export const baseApi = createApi(...)
Файл, который будет автоматически сгенерирован.
outputFile: "./src/store/api/generatedApi.ts"
Имя экспортируемого generated API.
exportName: "generatedApi"
Автоматическая генерация React hooks.
hooks: true
Создаются:
useGetUsersQuery
useCreateUserMutation
Добавление команды в package.json:
{
"scripts": {
"generate-api": "rtk-query-codegen-openapi openapi-config.ts"
}
}
Запуск:
npm run generate-api
После выполнения команды создается полноценный RTK Query API.
Пример:
export const generatedApi = baseApi.injectEndpoints({
endpoints: (build) => ({
getUsers: build.query<GetUsersApiResponse, GetUsersApiArg>({
query: () => ({
url: `/users`,
}),
}),
}),
});
Генератор создает hooks:
export const {
useGetUsersQuery,
useLazyGetUsersQuery,
} = generatedApi;
Использование:
const UsersPage = () => {
const { data, isLoading } = useGetUsersQuery();
if (isLoading) {
return <div>Loading...</div>;
}
return (
<div>
{data?.map(user => (
<div key={user.id}>
{user.name}
</div>
))}
</div>
);
};
RTK Query автоматически создает типы из OpenAPI schema.
export type User = {
id?: number;
name?: string;
email?: string;
};
export type GetUserApiArg = {
id: number;
};
export type GetUserApiResponse = User;
Ключевую роль играет:
operationId
/users:
get:
operationId: getUsers
Из этого генерируется:
useGetUsersQuery
Если operationId отсутствует:
/users:
get:
генератор создаст менее читаемые имена:
useUsersQuery
или:
useGetUsers2Query
Поэтому operationId рекомендуется задавать всегда.
/users:
post:
operationId: createUser
createUser: build.mutation<
CreateUserApiResponse,
CreateUserApiArg
>({
query: (body) => ({
url: `/users`,
method: "POST",
body,
}),
}),
const [createUser, result] = useCreateUserMutation();
OpenAPI:
/users/{id}:
get:
operationId: getUser
parameters:
- in: path
name: id
required: true
schema:
type: integer
query: (queryArg) => ({
url: `/users/${queryArg.id}`,
}),
type GetUserApiArg = {
id: number;
};
/users:
get:
parameters:
- in: query
name: page
schema:
type: integer
- in: query
name: lim it
schema:
type: integer
query: (queryArg) => ({
url: `/users`,
params: {
page: queryArg.page,
lim it: queryArg.limit,
},
}),
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/CreateUserDto"
query: (body) => ({
url: `/users`,
method: "POST",
body,
}),
Role:
type: string
enum:
- admin
- user
- moderator
export type Role =
| "admin"
| "user"
| "moderator";
email:
type: string
nullable: true
email?: string | null;
OpenAPI поддерживает сложные композиции схем.
User:
allOf:
- $ref: "#/components/schemas/BaseEntity"
- type: object
properties:
email:
type: string
type User = BaseEntity & {
email?: string;
};
oneOf:
- $ref: "#/components/schemas/Cat"
- $ref: "#/components/schemas/Dog"
type Animal = Cat | Dog;
Большие API могут генерироваться частями.
export default defineConfig({
schemaFile: "./openapi.json",
apiFile: "./src/store/api/baseApi.ts",
outputFiles: {
"./src/store/api/users.ts": {
filterEndpoints: [
"getUsers",
"getUser",
],
},
"./src/store/api/posts.ts": {
filterEndpoints: [
"getPosts",
"createPost",
],
},
},
hooks: true,
});
Позволяет выбрать конкретные endpoints.
filterEndpoints: [
"getUsers",
"createUser",
]
Иногда hooks не нужны.
Например:
hooks: false
Generated endpoints можно расширять.
export const api = generatedApi.enhanceEndpoints({
endpoints: {
getUsers: {
transformResponse: (
response: User[]
) => {
return response.sort(
(a, b) => a.id - b.id
);
},
},
},
});
Generated API поддерживает кеширование.
export const baseApi = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "/api",
}),
tagTypes: [
"User",
"Post",
],
endpoints: () => ({}),
});
export const extendedApi =
generatedApi.injectEndpoints({
endpoints: (build) => ({
uploadAvatar: build.mutation({
query: (file) => {
const formData = new FormData();
formData.append("file", file);
return {
url: "/avatar",
method: "POST",
body: formData,
};
},
}),
}),
});
Позволяет переопределять generated endpoints.
overrideExisting: true
Базовая авторизация настраивается в baseApi.
export const baseApi = createApi({
reducerPath: "api",
baseQuery: fetchBaseQuery({
baseUrl: "/api",
prepareHeaders: (headers, { getState }) => {
const token =
getState().auth.token;
if (token) {
headers.set(
"Authorization",
`Bearer ${token}`
);
}
return headers;
},
}),
endpoints: () => ({}),
});
Все generated endpoints автоматически используют эту конфигурацию.
OpenAPI может описывать multipart/form-data.
content:
multipart/form-data:
schema:
type: object
properties:
file:
type: string
format: binary
query: (body) => ({
url: `/upload`,
method: "POST",
body,
}),
Генерацию обычно добавляют:
{
"scripts": {
"prebuild": "npm run generate-api",
"build": "vite build"
}
}
Частая практика:
В monorepo OpenAPI schema может лежать:
packages/api/openapi.json
Frontend:
schemaFile:
"../. ./packages/api/openapi.json"
В крупных проектах codegen обычно используется вместе с:
OpenAPI становится единым источником правды.
Сначала создается контракт:
/users:
get:
Затем:
Ошибки OpenAPI приводят к проблемам генерации:
type: strng
Без operationId возникают:
User:
properties:
posts:
items:
$ref: "#/components/schemas/Post"
Post:
properties:
author:
$ref: "#/components/schemas/User"
Некоторые генераторы могут создавать слишком сложные типы.
Лучше разделять:
Хорошая практика:
operationId: getUserById
Плохая:
operationId: users
/v1/users
/v2/users
useGetV1UsersQuery
useGetV2UsersQuery
Generated types особенно полезны при:
{
"strict": true
}
TypeScript начинает контролировать:
Контракты синхронизированы автоматически.
Новые endpoints появляются практически мгновенно.
OpenAPI становится единым источником данных.
Типизируются:
Codegen особенно эффективен: