Спецификация OpenAPI

OpenAPI — это спецификация для документирования API в формате YAML или JSON. Она позволяет стандартизированно описывать REST API.

Swagger UI и ReDoc — это популярные инструменты, которые преобразуют спецификацию OpenAPI в интерактивную документацию (UI для спецификации OpenAPI).

Как найти исходную спецификацию OpenAPI по Swagger UI или ReDoc? Открыв документацию Swagger UI или ReDoc в браузере, в консоли разработчика можно найти сетевые запросы к файлу спецификации (формата *.json, *.yaml или *.yml и содержимое будет начинаться с версии спецификации OpenAPI).

Как найти исходную спецификацию OpenAPI по Swagger UI или ReDoc?

Как найти исходную спецификацию OpenAPI по Swagger UI или ReDoc?


Как можно применить спецификацию OpenAPI?

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

Одно из главных преимуществ — возможность генерировать TypeScript типы (для всех эндпоинтов, request/response моделей) из спецификации. Особенно полезно в небольших стартапах или на начальных этапах разработки проекта, когда api быстро меняется, а изменения не всегда обратно совместимы, и вручную уследить за всеми изменениями сложно.

Примеры инструментов:

  • openapi-typescript - генерирует чистые TypeScript-типы (paths, components) из OpenAPI 3.0/3.1 без runtime-кода, быстро и с поддержкой YAML/JSON (локально/удалённо);

  • dtsgenerator - Создаёт .d.ts файлы из JSON Schema или OpenAPI (v2/v3), с CLI, плагинами и поддержкой stdin/url;

  • openapi-generator - Универсальный генератор (CLI @openapitools/openapi-generator-cli) для клиентов, серверов, документации в 50+ языках, включая TypeScript-варианты (axios, fetch);

1.1. Пример генерации типов для RTK

Полный код рабочего примера на github

Как это работает:

1.1.1. Генерируем типы из OpenAPI спецификации через npm команду

// ...
"scripts": {
  //...
  "generate-types": "dtsgen --out src/api/types/generated-counter-api-types.d.ts specs/prepared-counter-api.yaml",
  //...
}
// ...

1.1.2. Генерируются файлы с API типами

// ...

declare namespace Components {
    namespace Schemas {
        export interface CounterResponse {
            value: number; // int32
        }
        export interface CounterRequest {
            value: number; // int32
        }
    }
}

declare namespace Paths {
    namespace GetCounter {
        export interface QueryParameters {
            version?: string;
        }
        namespace Responses {
            export type $200 = Components.Schemas.CounterResponse;
        }
    }
    namespace SaveCounter {
        export type RequestBody = Components.Schemas.CounterRequest;
        namespace Responses {
            export type $200 = Components.Schemas.CounterResponse;
        }
    }
}

// ...

1.1.3. Создаём RTK Query API с использованием сгенерированных типов файл

/// <reference path="../../api/types/generated-counter-api-types.d.ts" />

// ...

export const counterApi = createApi({
  // ...
  endpoints: (builder) => ({
    getCounter: builder.query<Paths.GetCounter.Responses.$200, Paths.GetCounter.QueryParameters>({
      // ...
    }),
    saveCounter: builder.mutation<
      Paths.SaveCounter.Responses.$200,
      Paths.SaveCounter.RequestBody
    >({
      // ...
    }),
  }),
});

// ...

1.1.4. Используем типобезопасные хуки в React компонентах файл

// ...

const { data, isLoading, error } = useGetCounterQuery({ version });
const [saveCounter] = useSaveCounterMutation();

// data имеет тип Paths.GetCounter.Responses.$200
// saveCounter принимает параметры типа Paths.SaveCounter.RequestBody

// ...

1.2. Пример генерации типов для react-query

Полный код рабочего примера на github

Как это работает детально:

1.2.1. Генерируем типы из OpenAPI спецификации через npm команду

// ...
"scripts": {
  // ...
  "generate-types": "dtsgen --out src/api/types/generated-api-types.d.ts specs/prepared-openapi.json"
  // ...
}
// ...

1.2.2. Генерируются файлы с API типами

declare namespace Components {
    namespace Schemas {
        export interface Pet {
            id?: number; // int64
            name: string;
            photoUrls: string[];
            status?: "available" | "pending" | "sold";
            category?: Category;
            tags?: Tag[];
        }
    }
}

declare namespace Paths {
    namespace GetPetById {
        export interface PathParameters {
            petId: number; // int64
        }
        namespace Responses {
            export type $200 = Components.Schemas.Pet;
        }
    }
}

1.2.3. Создаём API-функции с использованием сгенерированных типов

/// <reference path="./types/generated-api-types.d.ts" />

export async function fetchPetById(petId: number): Promise<Paths.GetPetById.Responses.$200> {
  const res = await fetch(`http://localhost:4010/api/v3/pet/${petId}`);
  if (!res.ok) {
    throw new Error('Pet is not found');
  }
  return res.json();
}

1.2.4. Используем типобезопасные хуки React Query в компонентах

import { useQuery } from '@tanstack/react-query';
import { fetchPetById } from './api/fetchPetById';

// ...

function App() {
  // ...
  const { isPending, error, data } = useQuery({
    queryKey: ['pet', petId],
    queryFn: () => fetchPetById(petId),
  });
  // ...
}

1.3. Пример генерации типов для нативного fetch

Полный код рабочего примера на github

Как это работает детально:

1.3.1. Генерируем типы из OpenAPI спецификации через npm команду

// ...
"scripts": {
  //...
  "generate-types": "dtsgen --out src/api/types/generated-api-types.d.ts specs/prepared-openapi.json",
  //...
}
// ...

1.3.2. Генерируются файлы с API типами

// ...

declare namespace Components {
    namespace Schemas {
        export interface Pet {
            id?: number; // int64
            name: string;
            photoUrls: string[];
            status?: "available" | "pending" | "sold";
            category?: Category;
            tags?: Tag[];
        }
    }
}

declare namespace Paths {
    namespace GetPetById {
        export interface PathParameters {
            petId: number; // int64
        }
        namespace Responses {
            export type $200 = Components.Schemas.Pet;
            export interface $404 {}
        }
    }
}

// ...

1.3.3. Создаём типобезопасную обёртку над нативным fetch с использованием сгенерированных типов файл

/// <reference path="./types/generated-api-types.d.ts" />

export class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async get<T, E = unknown>(path: string, options?: RequestInit): Promise<ApiResult<T, E>> {
    const url = `${this.baseUrl}${path}`;
    const res = await fetch(url, { ...options, method: 'GET' });
    if (!res.ok) {
      const error = new ApiError(res.statusText, res.status);
      return [error, null];
    }
    const data = (await res.json()) as T;
    return [null, data];
  }

  // Специфичный метод с использованием сгенерированных типов
  async fetchPetByIdSafe(petId: number): Promise<ApiResult<Paths.GetPetById.Responses.$200, Paths.GetPetById.Responses.$404>> {
    return this.get<Paths.GetPetById.Responses.$200, Paths.GetPetById.Responses.$404>(`/api/v3/pet/${petId}`);
  }
}

1.3.4. Используем типобезопасный API клиент в React компонентах файл

import { ApiPet, ApiTag } from './api/types/models';
import { ApiClient, ApiError } from './api/client';

// ...
const apiClient = new ApiClient('http://localhost:4010');
// ...

function App() {
  // ...

  const [data, setData] = useState<ApiPet | null>(null);
  const [isLoading, setIsLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  // ...
 
  useEffect(() => {
    const fetchPet = async () => {
      setIsLoading(true);
      setError(null);
      const [err, pet] = await apiClient.fetchPetByIdSafe(petId);
      if (err) {
        setError(err.message);
      } else {
        setData(pet);
      }
      setIsLoading(false);
    };

    fetchPet();
  }, [petId]);

  // ...
}

2. Генерация API-клиентов

Из OpenAPI спецификации можно генерировать готовые API-клиенты.

Не нужно вручную писать fetch/axios запросы, автоматическая обработка headers, query параметров.

Примеры инструментов:

2.1. Пример сгенерированного клиента с использованием orval

Полный код рабочего примера на github

Как это работает детально:

2.1.1. Пишем конфигурацию

export default defineConfig({
  api: {
    input: {
      target: './specs/prepared-openapi.json',
    },
    output: {
      target: './src/api/generated/api.ts',
      client: 'fetch',
      mode: 'tags',
      baseUrl: 'http://localhost:4010',
    },
  },
});

2.1.2. Запускаем генерацию через npm комманду

// ...

"scripts": {
  //...
  "generate-types": "orval",
  //...
}

// ...

2.1.3. Генерируются файлы с api клиентами

// ...

export const getPetById = async (petId: number, options?: RequestInit): Promise<getPetByIdResponse> => {
   // ...
   const res = await fetch(getGetPetByIdUrl(petId));
   // ...
}

// ...

2.1.4. Генерируются файлы с api типами

// ...

export interface Pet {
  category?: Category;
  id?: number;
  name: string;
  photoUrls: string[];
  /** pet status in the store */
  status?: PetStatus;
  tags?: Tag[];
}

// ...

3. Типизация endpoints в BFF-прокси на Node.js с Express

Автоматическая типизация запросов/ответов (генерируя общие TypeScript-типы из API-контрактов из OpenAPI спецификации), исключает ошибки парсинга данных на фронте и упрощает интеграцию с React/TypeScript без ручного описания интерфейсов. Фронтенд получает “глупый” клиент с полной автодополняемостью и валидацией на этапе компиляции, снижая время на отладку API-интеграций. Это особенно полезно в микросервисах, где BFF адаптирует backend-API под нужды UI.

3.1. Пример типизации express endpoints через общие TypeScript-типы

Полный код рабочего примера на github

Как это работает:

3.1.1. Генерируем типы из OpenAPI спецификации через npm команду

"scripts": {
  //...
  "generate-types": "dtsgen --out src/services/petstore/generated-api-types.d.ts specs/prepared-petstore.json",
  //...
}

3.1.2. Генерируются файлы с API типами

// ...

declare namespace PetstoreComponents {
    namespace Schemas {
    	// ...
    	export interface Order {
            complete?: boolean;
            /**
             * example:
             * 10
             */
            id?: number; // int64
            /**
             * example:
             * 198772
             */
            petId?: number; // int64
            /**
             * example:
             * 7
             */
            quantity?: number; // int32
            shipDate?: string; // date-time
            /**
             * Order Status
             * example:
             * approved
             */
            status?: "placed" | "approved" | "delivered";
        }
    	// ...
    }
}

declare namespace PetstorePaths {
	// ...
    namespace StoreOrder$OrderId {
        namespace Get {
            namespace Parameters {
                export type OrderId = number; // int64
            }
            export interface PathParameters {
                orderId: Parameters.OrderId /* int64 */;
            }
            namespace Responses {
                export type $200 = PetstoreComponents.Schemas.Order;
                export interface $400 {
                    id?: number; // int64
                }
                export interface $404 {
                    message?: string;
                }
            }
        }
    }
    // ...
}

3.1.3. Определяем типобезопасный контроллер через интерфейс TypedController

// ...
export type TypedController<T extends ExpressRouteShape> = (
    request: TypedRequest<T>,
    response: TypedResponse<T>,
    next: TypedNext<T>,
) => Promise<void> | void;
// ...

3.1.4. Создаём типобезопасные контроллеры с использованием сгенерированных типов

// Определяем тип маршрута с параметрами запроса и возможными ответами

type GetOrderByIdRoute = {
  request: {
    params: {
      orderId: PetstorePaths.StoreOrder$OrderId.Get.Parameters.OrderId;
    };
  };
  responses: {
    success: PetstorePaths.StoreOrder$OrderId.Get.Responses.$200;
    error:
        | PetstorePaths.StoreOrder$OrderId.Get.Responses.$400
        | PetstorePaths.StoreOrder$OrderId.Get.Responses.$404; 
  };
}

// ...

// Типизированный контроллер для маршрута GetOrderById
const getOrderByIdRouteTypedController: TypedController<GetOrderByIdRoute> = async (request, response, next) => {
	// ...

	const responseMock = await getMockFromOpenApi<PetstorePaths.StoreOrder.Post.Responses.$200>('post', '/store/order', '200');
    
	// ...
    response.result = responseMock;
    next();

    // ...

    // Example: not valid response body
    // Intentionally assign an invalid object to demonstrate TypeScript error checking
    // @ts-expect-error: TS2322: Type '{ foo: string; }' is not assignable to type 'Order'. Object literal may only specify known properties, and 'foo' does not exist in type 'Order'.
    response.result = { foo: 'bar'};
}

// Регистрируем маршрут с типобезопасным контроллером
app.get('/store/order/:orderId', getOrderByIdRouteTypedController);

3.1.5. Примеры типобезопасных контроллеров для разных типов запросов:

POST запрос с телом:

type PlaceOrderRoute = {
    request: {
        body: PetstorePaths.StoreOrder.Post.RequestBody;
    };
    responses: {
        success: PetstorePaths.StoreOrder.Post.Responses.$200;
        error:
            | PetstorePaths.StoreOrder.Post.Responses.$400
            | PetstorePaths.StoreOrder.Post.Responses.$422;
    };
}

const placeOrderRouteTypedController: TypedController<PlaceOrderRoute> = async (request, response, next) => {
    const order = request.body; // order типизирован как PetstoreComponents.Schemas.Order
    // ...
}

GET запрос с query параметрами:

type LoginUserRoute = {
    request: {
        query: PetstorePaths.UserLogin.Get.QueryParameters;
    };
    responses: {
        success: PetstorePaths.UserLogin.Get.Responses.$200;
        error: PetstorePaths.UserLogin.Get.Responses.$400;
    };
}

const loginUserRouteTypedController: TypedController<LoginUserRoute> = async (request, response, next) => {
    const username = request.query.username; // типизирован как string | undefined
    const password = request.query.password; // типизирован как string | undefined
    // ...
}

Примеры других инструментов:

  • tRPC https://trpc.io/ - Полная end-to-end типизация без схем; интегрируется с Express, идеален для React+BFF; типы генерируются автоматически из роутеров

  • routing-controllers https://github.com/typestack/routing-controllers - Декораторы для классов-контроллеров в Express; типизирует handlers с @Get/@Post, поддерживает валидацию

4. Валидация в обработчиках (handlers) Express

OpenAPI спецификация значительно упрощает валидацию в обработчиках (handlers) Express, обеспечивая автоматическую проверку запросов и ответов на соответствие заранее определённым схемам.

Примеры библиотек:

4.1. Пример валидации запросов

Полный код рабочего примера на github

Как это работает:

Нужно добавить middleware для валидации запросов и в аргументы пробросить OpenApi спецификацию:

app.use(
  OpenApiValidator.middleware({
    apiSpec: openApiSpec,
    validateRequests: true,
    validateResponses: true,
  }),
);

5. Генерация моковых/sample данных/сущностей для разработки и интеграционных тестов

OpenApi спецификация может использоваться для автоматической генерации сущностей и моделей данных, которые можно использовать в интеграционных тестах, обеспечивая их согласованность с контрактом API и ускоряя QA-процессы.

Примеры инструментов:

5.1. Пример генерации моковых данных

Полный код рабочего примера на github

Генерация моковых/sample данных основана на конвертации OpenAPI схем в JSON Schema и использовании json-schema-faker https://www.npmjs.com/package/json-schema-faker:

  • @openapi-contrib/openapi-schema-to-json-schema – конвертирует OpenAPI схемы в JSON Schema

  • @apidevtools/json-schema-ref-parser – разрешает все $ref ссылки в OpenAPI спецификации

  • json-schema-faker – генерирует валидные данные на основе JSON Schema

Как это работает детально:

5.1.1. Генерация sample данных для сущностей (компонентов) из OpenAPI:

// Утилита для генерации sample объекта для сущности из OpenAPI спецификации
// ...
async function createEntitySampleFromOpenApi<T>(entityName: string): Promise<T> {
  // Загружаем OpenAPI спецификацию
  const specPath = path.join(__dirname, '../specs/petstore.json');
  const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
  // Разрешаем все $ref ссылки
  const dereferenced: any = await RefParser.dereference(spec);
  // Находим схему для указанной сущности/компонента
  const entitySchema = dereferenced.components.schemas[entityName];
  if (!entitySchema) throw new Error('Component not found');
  // Конвертируем OpenAPI схему в JSON Schema
  const jsonSchema = openapiSchemaToJsonSchema(entitySchema);
  // Генерируем sample объект, соответствующий схеме
  return jsf.generate(jsonSchema) as T;
}

// Использование в тестах
it('should generate a valid Pet object', async () => {
  // Генерируем sample объект для сущности 'Pet', определённой в OpenAPI спецификации
  const sample = await createEntitySampleFromOpenApi<PetstoreComponents.Schemas.Pet>('Pet');
  // sample типизирован как PetstoreComponents.Schemas.Pet
  expect(sample).toBeTruthy();
});

5.1.2. Генерация sample данных для ответов endpoint’ов:

// Утилита для генерации sample ответа для endpoint'а из OpenAPI спецификации
async function createEndpointResponseSampleFromOpenApi<T>(method: string, endpointPath: string, statusCode: string): Promise<T> {
  // Загружаем OpenAPI спецификацию
  const specPath = path.join(__dirname, '../specs/petstore.json');
  const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
  // Разрешаем все $ref ссылки
  const dereferenced: any = await RefParser.dereference(spec);
  // Находим указанный endpoint
  const pathItem = dereferenced.paths[endpointPath];
  if (!pathItem) throw new Error('Path not found');
  // Находим указанный HTTP метод
  const op = pathItem[method.toLowerCase()];
  if (!op) throw new Error('Method not found');
  // Извлекаем ответ для указанного статус кода
  const resp = op.responses && op.responses[statusCode];
  if (!resp) throw new Error('Status code not found');
  // Получаем JSON схему для ответа
  const content = resp.content && resp.content['application/json'];
  if (!content || !content.schema) throw new Error('Response schema not found');
  // Конвертируем OpenAPI схему в JSON Schema
  const jsonSchema = openapiSchemaToJsonSchema(content.schema);
  // Генерируем sample объект, соответствующий схеме
  return jsf.generate(jsonSchema) as T;
}

// Использование в тестах
it('should generate a valid endpoint response', async () => {
  // Генерируем sample ответ для GET /pet/{petId} endpoint со статусом 200
  const sampleResponse = await createEndpointResponseSampleFromOpenApi<PetstorePaths.Pet$PetId.Get.Responses.$200>(
    'get', 
    '/pet/{petId}', 
    '200'
  );
  // sampleResponse типизирован как PetstorePaths.Pet$PetId.Get.Responses.$200
  expect(sampleResponse).toBeTruthy();
});

5.1.3. Генерация моковых данных для использования в контроллерах:

// Утилита для генерации моковых данных из OpenAPI (используется в контроллерах)
async function getMockFromOpenApi<T>(method: string, endpointPath: string, statusCode: string): Promise<T> {
  const specPath = path.join(__dirname, '../specs/petstore.json');
  const spec = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
  const dereferenced: any = await RefParser.dereference(spec);
  const pathItem = dereferenced.paths[endpointPath];
  const op = pathItem[method.toLowerCase()];
  const resp = op.responses && op.responses[statusCode];
  const content = resp.content && resp.content['application/json'];
  const jsonSchema = openapiSchemaToJsonSchema(content.schema);
  return jsf.generate(jsonSchema) as T;
}

// Использование в контроллере
const getOrderByIdRouteTypedController: TypedController<GetOrderByIdRoute> = async (request, response, next) => {
    // ...

  // Генерируем моковый ответ для успешного ответа 200
  const responseMock = await getMockFromOpenApi<PetstorePaths.StoreOrder$OrderId.Get.Responses.$200>(
    'get', 
    '/store/order/{orderId}', 
    '200'
  );
  response.result = responseMock; // типобезопасно
  next();

// ...

  // Example 4: valid error body
  // Generate a mock error response for a 400 error using OpenAPI types
  const responseErrorMock = await getMockFromOpenApi<PetstorePaths.StoreOrder$OrderId.Get.Responses.$400>('post', '/store/order/{orderId}', '400');
    next(
        new ServiceResponseError({
            statusCode: 400,
            body: responseErrorMock
        }),
    );

    // ...
};


6. Создание mock-серверов

OpenAPI спецификация значительно упрощает создание mock-серверов, позволяя автоматически генерировать имитацию API на основе описанных эндпоинтов, схем данных и примеров ответов. Инструменты вроде MockServer, Smogger или Swagger Mock загружают спецификацию (в формате YAML/JSON) и динамически создают сервер, который возвращает реалистичные мок-ответы с рандомными данными по схемам, без необходимости писать код вручную. Это ускоряет frontend-разработку, тестирование и интеграцию, пока реальный бэкенд не готов, обеспечивая валидацию запросов и поддержку всех HTTP-методов из спецификации.

Примеры инструментов:

  • Prism (@stoplight/prism-cli) https://github.com/stoplightio/prism запускает полноценный HTTP mock/proxy-сервер из OpenAPI 2/3 или Postman, с динамическими ответами, валидацией и transformations

  • msw + msw-auto-mock https://www.npmjs.com/package/msw-auto-mock - перехватывает запросы в браузере/Node (Service Worker), генерируя TS-моки из OpenAPI с faker.js данными для dev-тестов

  • json-server https://www.npmjs.com/package/json-server - создаёт REST API из JSON-файла (db.json), с CRUD, auth-плагинами (json-server-auth), но без native OpenAPI — нужны кастомные плагины для схем

6.1. Пример запуска mock-сервера

Полный код рабочего примера на github

Как это работает детально:

Запускаем mock-сервер с помощью Prism CLI https://github.com/stoplightio/prism:

"scripts": {
  //...
  "mock:api": "prism mock specs/prepared-openapi.json -h 0.0.0.0 -p 4010 --cors",
  //...
}

// npm run mock:api

Mock-сервер:

  • Читает OpenAPI спецификацию из specs/prepared-openapi.json

  • Автоматически генерирует валидные ответы на основе схем из спецификации

  • Запускается на http://localhost:4010

  • Поддерживает CORS для работы с фронтенд-приложением

  • Эмулирует все endpoint’ы, определённые в OpenAPI спецификации


Возникающие типичные проблемы и как их решить

Фронтендеры часто сталкиваются с тем, что OpenAPI документация от backend-команды неидеальна и мешает эффективной работе:

  • допускаются банальные опечатки, если бэкендеры пишут спецификацию вручную;

  • стандарт OpenAPI формальный, из-за этого иногда спецификацию пишут так, что ломаются интеграции с инструментами (например, из-за особенностей написания спецификации перестаёт работать генерация типов);

  • если бэкендеры генерируют спецификацию из кода, она может формироваться так, что ломаются интеграции с инструментами работы со спецификацией — возникает дилемма: либо дорабатывать фронтенд-инструменты, либо исправлять генерацию спецификации на бэкенде.

В результате часть автоматизации теряет смысл, требуя ручных правок и уточнений.

Есть несколько способов решить эту проблему:

1. Валидировать OpenApi спецификацию

Инструменты для валидации OpenApi спецификации:

  • Redocly CLI выполняет linting с рекомендациями по улучшению, поддерживает OpenAPI 3.x/2.0, bundling, документацию и API contract testing через Respect.

  • Spectral — это линтер на основе правил (rulesets/functions), ориентированный на OpenAPI, AsyncAPI и другие JSON/YAML, с высокой кастомизацией.

  • Swagger CLI в основном валидирует схему OpenAPI 3.0/Swagger 2.0 плюс базовые проверки (дубликаты, MIME-типы), но устарел и не развивается

При помощи валидации спецификации можно вовремя заметить ломающие изменения, но это не решает проблему неидеальности OpenAPI-спецификации.

2. Подготавливать OpenApi спецификацию

У Redocly CLI для модификации спецификации используется функция decorators, которая позволяет трансформировать документ (добавлять примеры, описания или изменять URL).

Decorators подходят для простых трансформаций (удаление эндпоинтов, добавление примеров), но для сложных случаев требуют custom plugins на JS, что усложняет поддержку.

Для упрощения подготовки OpenAPI-спецификации можно использовать openapi-modifier — он содержит широкий набор простых правил для трансформаций, которые применяются последовательно (собираются в pipeline). Таким образом можно строить сложные трансформации.

Пример конфигурации openapi-modifier:

module.exports = {
  input: "./openapi.yaml",
  output: "./openapi.mod.yaml",
  logger: { verbose: true },
  pipeline: [
    // JIRA-232: добавляем ручки новой фичи
    { rule: "merge-openapi-spec", config: { path: "docs/feature-JIRA-232.yaml" } },

    // JIRA-8752: исправляем тип id, сохраняя остальные поля Pet
    { rule: "patch-component-schema", config: { descriptor: "Pet.id", patchMethod: "merge", schemaDiff: { type: "string", format: "uuid" } } },

    // Нормализуем форматы перед изменением JSON-ответа
    { rule: "change-content-type", config: { map: { "*/*": "application/json" } } },
    { rule: "filter-by-content-type", config: { disabled: ["multipart/form-data"] } },

    // Исправляем статусы в ответе — массиве объектов без $ref
    { rule: "patch-endpoint-response-schema", config: { endpointDescriptor: "GET /v1/pets", code: "200", contentType: "application/json", correction: "[].status", patchMethod: "merge", schemaDiff: { enum: ["active", "archived"] } } },

    // Нормализация под генераторы
    { rule: "remove-operation-id" },
    { rule: "remove-min-items" },

    // Убираем deprecated ручки
    { rule: "remove-deprecated", config: { showDeprecatedDescriptions: true } },
    // Убираем внутренние ручки
    { rule: "filter-endpoints", config: { disabledPathRegExp: [/^\/internal(?:\/|$)/] } },
    // Удаляем неиспользуемые компоненты
    { rule: "remove-unused-components", config: { printDeletedComponents: true } },
  ],
};

openapi-modifier позволяет держать спецификацию в пригодном для кодогенерации состоянии независимо от темпа и качества исходной документации, а знания об исправлениях хранить декларативно, прозрачно и воспроизводимо.

Типичные ситуации и как их решить:

Ниже — независимые примеры конфигураций. Имена моделей, полей и пути условные; замените их на свои. Входной и выходной файлы можно передать через CLI: npx openapi-modifier --input openapi.yaml --output prepared-openapi.yaml --config openapi-modifier.config.js.

  • Бэкенд указал неверный тип int вместо integer, что ломает кодогенерацию TS-типов или API-клиентов Пример конфигурации:

module.exports = {
  pipeline: [{
    rule: 'patch-component-schema',
    config: {
      // Исправляем тип поля id в модели Pet.
      descriptor: 'Pet.id',
      patchMethod: 'merge',
      schemaDiff: { type: 'integer' },
    },
  }],
};
  • Бэкенд допустил опечатку в формате поля, например указал datetime вместо date-time Пример конфигурации:

module.exports = {
  pipeline: [{
    rule: 'patch-component-schema',
    config: {
      // Исправляем формат даты в поле createdAt.
      descriptor: 'Pet.createdAt',
      patchMethod: 'merge',
      schemaDiff: { type: 'string', format: 'date-time' },
    },
  }],
};
  • Бэкенд забыл указать "type": "object" в схеме модели, что ломает кодогенерацию TS-типов или API-клиентов Пример конфигурации:

module.exports = {
  pipeline: [{
    rule: 'patch-component-schema',
    config: {
      // Указываем пропущенный тип модели Pet.
      descriptor: 'Pet',
      patchMethod: 'merge',
      schemaDiff: { type: 'object' },
    },
  }],
};
  • Бэкенд на проде находится под другой прокси, и нужно поменять basepath для всех endpoint в спецификации Пример конфигурации:

module.exports = {
  pipeline: [{
    rule: 'change-endpoints-basepath',
    // /pets → /api/v1/pets; меняются ключи paths, а не servers.
    config: { map: { '/': '/api/v1/' } },
  }],
};
  • Мешают minItems и maxItems для генерации типов или API-клиентов Пример конфигурации:

module.exports = {
  pipeline: [
    // Убираем ограничение на минимальную длину массивов.
    { rule: 'remove-min-items' },
    // Убираем ограничение на максимальную длину массивов.
    { rule: 'remove-max-items' },
  ],
};
  • Мешает operationId для генерации типов или API-клиентов Пример конфигурации:

module.exports = {
  pipeline: [
    // Удаляем operationId у всех операций.
    { rule: 'remove-operation-id' },
  ],
};
  • Нужно убрать лишние варианты ответов application/xml и переписать / на application/json для генерации типов или API-клиентов Пример конфигурации:

module.exports = {
  pipeline: [
    {
      // Убираем XML из запросов и ответов.
      rule: 'filter-by-content-type',
      config: { disabled: ['application/xml'] },
    },
    {
      // Заменяем */* на application/json.
      rule: 'change-content-type',
      config: { map: { '*/*': 'application/json' } },
    },
  ],
};
  • Не нужны все ручки из OpenAPI-спецификации, к примеру, нужно убрать /private/* ручки из документации для фронтенда. Это не ограничивает доступ к реальным эндпоинтам Пример конфигурации:

module.exports = {
  pipeline: [{
    // Исключаем /private и все пути внутри /private/.
    rule: 'filter-endpoints',
    config: { disabledPathRegExp: [/^\/private(?:\/|$)/] },
  }],
};
  • Параметр или свойство метода скоро исчезнет (оно еще не помечено как deprecated в документации), и нужно отредактировать спецификацию, чтобы после перегенерации типов или API-клиентов TypeScript подсказал, где нужно убрать его использование, и дополнительно не допустить повторного его использования Пример конфигурации:

module.exports = {
  pipeline: [{
    // Удаляем query-параметр version у GET /pets.
    rule: 'remove-parameter',
    config: {
      endpointDescriptor: 'GET /pets',
      parameterDescriptor: { name: 'version', in: 'query' },
    },
  }],
};
  • Параметр или свойство метода скоро появится, и нужно добавить его, но в спецификации бэкенда его еще нет, а разработку фронтенда нужно начинать — нужно отредактировать спецификацию, чтобы после перегенерации типов или API-клиентов новый параметр появился в них Пример конфигурации:

module.exports = {
  pipeline: [{
    rule: 'patch-component-schema',
    config: {
      descriptor: 'Pet',
      patchMethod: 'deepmerge',
      schemaDiff: {
        // Добавляем необязательное поле color, сохраняя остальные поля.
        properties: { color: { type: 'string' } },
      },
    },
  }],
};

Помимо данных правил, есть еще правила:

  • удаление deprecated компонентов, эндпоинтов, параметров и свойств (см. правило remove-deprecated) - позволяет вовремя замечать устаревания используемых элементов спецификации, например, бекендеры помечают элементы deprecated флагом - фронтендеры, замечая, что они пропадают с openapi вовремя замечают и съезжают с них

  • удаляющее неиспользуемые компоненты/cхемы, на которые нет ссылок или ref’ов (см. правило remove-unused-components) - позволяет очистить спецификацию от неиспользуемых элементов спецификации, к примеру, если были отфильтрованы ручки и остались неиспользуемые компоненты

  • объединение нескольких спецификаций (см. правило merge-openapi-spec) - полезно в ситуации, когда появляется новая фича, документация для ручек которой еще не готова для влития в основную, а параллельную разработку можно начинать.

Преимущества, которые даёт openapi-modifier:

  • Автоматизация — не нужно вручную править OpenAPI-файлы после каждого обновления

  • Исправление опечаток — быстрое исправление банальных ошибок без ожидания фикса от бэкенда

  • Документирование причин — каждое правило можно снабдить комментарием с номером задачи

  • Улучшение коммуникации в команде — единый источник правды

  • Ускорение онбординга новых разработчиков — самодокументируемый код, все причины правок документированы

  • Pipeline-подход — правила применяются последовательно, можно строить сложные трансформации. Любое правило можно применять несколько раз — дробите фиксы на мелкие осмысленные блоки с комментариями и ссылками на тикеты.