Генерация из OpenAPI спецификации

Интеграция RTK Query с OpenAPI позволяет автоматически генерировать API-слой на основе спецификации сервера. Такой подход устраняет дублирование кода между backend и frontend, уменьшает количество ошибок при изменении контрактов API и значительно ускоряет разработку.

OpenAPI-спецификация описывает:

  • маршруты API;
  • параметры запросов;
  • типы ответов;
  • схемы данных;
  • методы авторизации;
  • HTTP-методы;
  • возможные коды ошибок.

RTK Query способен использовать эту спецификацию для автоматического создания:

  • endpoints;
  • query/mutation hooks;
  • типов TypeScript;
  • готовых методов API.

Проблемы ручного описания endpoints

При ручном создании API возникают типичные проблемы:

Рассинхронизация frontend и backend

Backend изменил поле:

{
  "username": "admin"
}

Frontend продолжает ожидать:

{
  "login": "admin"
}

В результате:

  • runtime-ошибки;
  • некорректный рендер;
  • проблемы сериализации;
  • скрытые баги.

Дублирование типов

Backend:

User:
  type: object
  properties:
    id:
      type: integer
    email:
      type: string

Frontend:

interface User {
    id: number;
    email: string;
}

Каждое изменение требует ручного обновления.


Огромное количество boilerplate-кода

При большом API приходится вручную создавать:

  • query;
  • mutation;
  • hooks;
  • типы;
  • трансформеры;
  • теги кеширования.

OpenAPI-генерация устраняет большую часть этой рутины.


OpenAPI Generator для RTK Query

Для генерации 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

Форматы OpenAPI

Генератор поддерживает:

  • OpenAPI 2.0 (Swagger);
  • OpenAPI 3.x;
  • YAML;
  • JSON.

Пример 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,
});

Назначение параметров

schemaFile

Источник OpenAPI-спефикации.

Возможные варианты:

schemaFile: "./openapi.yaml"

или:

schemaFile: "https://api.site.com/openapi.json"

apiFile

Файл, содержащий базовый RTK Query API.

Пример:

import { createApi, fetchBaseQuery } fr om "@reduxjs/toolkit/query/react";

export const baseApi = createApi({
    reducerPath: "api",

    baseQuery: fetchBaseQuery({
        baseUrl: "/api",
    }),

    endpoints: () => ({}),
});

apiImport

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

apiImport: "baseApi"

соответствует:

export const baseApi = createApi(...)

outputFile

Файл, который будет автоматически сгенерирован.

outputFile: "./src/store/api/generatedApi.ts"

exportName

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

exportName: "generatedApi"

hooks

Автоматическая генерация 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

Генератор создает 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.


Пример generated types

export type User = {
    id?: number;
    name?: string;
    email?: string;
};

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

export type GetUserApiArg = {
    id: number;
};

Типы ответов

export type GetUserApiResponse = User;

operationId и генерация имен

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

operationId

Пример

/users:
  get:
    operationId: getUsers

Из этого генерируется:

useGetUsersQuery

Отсутствие operationId

Если operationId отсутствует:

/users:
  get:

генератор создаст менее читаемые имена:

useUsersQuery

или:

useGetUsers2Query

Поэтому operationId рекомендуется задавать всегда.


Генерация mutations


OpenAPI

/users:
  post:
    operationId: createUser

Generated mutation

createUser: build.mutation<
    CreateUserApiResponse,
    CreateUserApiArg
>({
    query: (body) => ({
        url: `/users`,
        method: "POST",
        body,
    }),
}),

Generated hook

const [createUser, result] = useCreateUserMutation();

Работа с параметрами


Path parameters

OpenAPI:

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

    parameters:
      - in: path
        name: id
        required: true
        schema:
          type: integer

Generated query

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

Generated type

type GetUserApiArg = {
    id: number;
};

Query parameters


OpenAPI

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

      - in: query
        name: lim it
        schema:
          type: integer

Generated endpoint

query: (queryArg) => ({
    url: `/users`,
    params: {
        page: queryArg.page,
        lim it: queryArg.limit,
    },
}),

Request body


OpenAPI

requestBody:
  required: true

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

Generated mutation

query: (body) => ({
    url: `/users`,
    method: "POST",
    body,
}),

enum и union types


OpenAPI enum

Role:
  type: string

  enum:
    - admin
    - user
    - moderator

Generated TypeScript

export type Role =
    | "admin"
    | "user"
    | "moderator";

nullable поля


OpenAPI

email:
  type: string
  nullable: true

Generated type

email?: string | null;

allOf, oneOf, anyOf

OpenAPI поддерживает сложные композиции схем.


allOf

User:
  allOf:
    - $ref: "#/components/schemas/BaseEntity"

    - type: object
      properties:
        email:
          type: string

Generated intersection type

type User = BaseEntity & {
    email?: string;
};

oneOf

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

Generated union

type Animal = Cat | Dog;

split API generation

Большие 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,
});

filterEndpoints

Позволяет выбрать конкретные endpoints.

filterEndpoints: [
    "getUsers",
    "createUser",
]

Генерация без hooks

Иногда hooks не нужны.

Например:

  • SSR;
  • Node.js;
  • non-React приложения.

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

hooks: false

transformResponse после генерации

Generated endpoints можно расширять.


enhanceEndpoints

export const api = generatedApi.enhanceEndpoints({
    endpoints: {
        getUsers: {
            transformResponse: (
                response: User[]
            ) => {
                return response.sort(
                    (a, b) => a.id - b.id
                );
            },
        },
    },
});

Добавление tagTypes

Generated API поддерживает кеширование.


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

export const baseApi = createApi({
    reducerPath: "api",

    baseQuery: fetchBaseQuery({
        baseUrl: "/api",
    }),

    tagTypes: [
        "User",
        "Post",
    ],

    endpoints: () => ({}),
});

Расширение generated endpoints


injectEndpoints

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,
                    };
                },
            }),
        }),
    });

overrideExisting

Позволяет переопределять 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 автоматически используют эту конфигурацию.


Генерация multipart endpoints

OpenAPI может описывать multipart/form-data.


OpenAPI

content:
  multipart/form-data:
    schema:
      type: object

      properties:
        file:
          type: string
          format: binary

Generated endpoint

query: (body) => ({
    url: `/upload`,
    method: "POST",
    body,
}),

Автоматизация генерации

Генерацию обычно добавляют:

  • в CI/CD;
  • prebuild;
  • precommit;
  • npm scripts.

Пример prebuild

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

Генерация при изменении backend

Частая практика:

  1. Backend публикует новую OpenAPI schema.
  2. Frontend запускает codegen.
  3. TypeScript сразу показывает несовместимости.
  4. Ошибки исправляются до runtime.

Работа с monorepo

В monorepo OpenAPI schema может лежать:

packages/api/openapi.json

Frontend:

schemaFile:
    "../. ./packages/api/openapi.json"

Генерация в enterprise-проектах

В крупных проектах codegen обычно используется вместе с:

  • микросервисами;
  • BFF;
  • schema registry;
  • contract-first development;
  • API versioning.

Contract-first подход

OpenAPI становится единым источником правды.

Сначала создается контракт:

/users:
  get:

Затем:

  • backend реализует API;
  • frontend генерирует клиента;
  • QA тестирует контракт.

Проблемы генерации


Некорректная schema

Ошибки OpenAPI приводят к проблемам генерации:

type: strng

Отсутствие operationId

Без operationId возникают:

  • нечитабельные имена;
  • конфликты;
  • дубли.

Циклические ссылки

User:
  properties:
    posts:
      items:
        $ref: "#/components/schemas/Post"

Post:
  properties:
    author:
      $ref: "#/components/schemas/User"

Некоторые генераторы могут создавать слишком сложные типы.


Рекомендации по структуре API


Разделение схем

Лучше разделять:

  • DTO;
  • entities;
  • request models;
  • response models.

Явные operationId

Хорошая практика:

operationId: getUserById

Плохая:

operationId: users

Версионирование API


OpenAPI

/v1/users
/v2/users

Generated endpoints

useGetV1UsersQuery
useGetV2UsersQuery

OpenAPI и TypeScript strict mode

Generated types особенно полезны при:

{
  "strict": true
}

TypeScript начинает контролировать:

  • nullable;
  • optional;
  • union types;
  • несовместимые поля;
  • отсутствующие свойства.

Преимущества codegen

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

Контракты синхронизированы автоматически.


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

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


Централизация API

OpenAPI становится единым источником данных.


Полная типизация

Типизируются:

  • ответы;
  • запросы;
  • query params;
  • body;
  • enums;
  • headers.

Масштабируемость

Codegen особенно эффективен:

  • в больших API;
  • в enterprise-системах;
  • при большом количестве frontend-команд;
  • при микросервисной архитектуре.