Туториал

Как из OpenAPI сгенерировать JSON API-документацию, типы TypeScript и API-клиент: от Schema к codegen

Ручные доки, ручные типы и ручной fetch, которые плывут порознь, дороже самого эндпоинта. Держите OpenAPI Schema как контракт: доки, типы и клиент растут из одного JSON.

Дорогое в GET /invoices редко сам обработчик. Это три артефакта, которые тухнут порознь: человеческие API-доки, типы TypeScript для компилятора, обёртка fetch для рантайма. Команды всё ещё клеят пример JSON в wiki, заново пишут поля в types.ts и склеивают URL в api.ts. OpenAPI Schema складывает это в один машиночитаемый контракт. Вы ведёте openapi.json (или YAML); сайт доков, файл типов и клиент рождаются из него. Поле меняется один раз — двигаются все три. MCP и JSON Schema — входы инструментов; структурированный вывод — финальный ответ модели. Здесь только HTTP JSON API: от Schema к докам, типам и клиенту. Спека: OpenAPI 3.1. Генератор по умолчанию: openapi-typescript.

Одна Schema, три продукта: доки, типы, клиент

Сначала цель. OpenAPI-доки — не ещё одна книга Markdown, а отрисовка той же spec. Scalar, Swagger UI, Redoc — читатели. Типы TypeScript — не второй рукописный интерфейс, а проекция paths и components.schemas. API-клиент — не «ещё одна обёртка axios», а функции из operationId или путей с типами запроса и ответа. Если двое из трёх рукописные, Schema — плакат на стене.

Инструменты потом; контракт первым. Выгрузите spec из FastAPI, Nest, Spring или Go или сначала напишите openapi.json и реализуйте против него. Запрещено: сначала клиент, потом доки «по ощущению» вызывающих. Нет единого источника, CI не решит, кто прав. Если агент берёт REST как инструмент, он должен читать этот OpenAPI, а не второй MCP inputSchema — два контракта сразу разъедутся. Слои — в статье про MCP выше.

OpenAPI 3.1 ближе сажает components.schemas к JSON Schema 2020-12: nullable это type: ["string", "null"], не nullable: true из 3.0. Меньше «в доках необязательно, в типах обязательно». Объект в JSONVue должен быть тем, что ест генератор, а не диалектом при экспорте.

Продукт Откуда берётся Не писать руками
JSON API-докиТот же OpenAPI JSON, рендер Scalar / Swagger UIПример JSON и скриншоты в wiki
Типы TypeScriptopenapi-typescript выдаёт paths / componentsПараллельный interface Invoice
API-клиент / хукиopenapi-fetch, Orval или hey-api выдают функции вызоваСклеенные URL и as Invoice

Сначала правильный OpenAPI JSON: paths, components, operationId

Генератор не обгонит качество spec. Минимум: версия openapi, info, paths, общие модели в components.schemas. Каждой операции — стабильный operationId (listInvoices, createInvoice); клиент берёт его как имя функции. Путь сменился — id не гоняйте. На запросе и успехе объявите application/json и $ref на компоненты, не копируйте таблицу полей на каждый path.

Ошибки тоже в контракте. Дайте JSON Schema на 400 / 401 / 404 / 422, чтобы клиент собрал читаемый union, а не unknown. Query и path — в parameters; не прячьте в body и не договаривайтесь устно в доках. Bearer/OIDC — в securitySchemes, чтобы сгенерированный клиент знал, куда класть токен. Сам протокол auth: OAuth 2.1 — в REST токен тоже в заголовке, не в JSON-конверте.

Ниже минимальный invoices API. Это не продуктовая spec, но хватает, чтобы крутить доки, типы и клиент разом. Invoice появляется один раз; list и create на него ссылаются. Сначала локально отформатируйте и проверьте parse, потом что $ref не оборваны.

{
  "openapi": "3.1.1",
  "info": {
    "title": "Invoices API",
    "version": "2026.09.18"
  },
  "paths": {
    "/invoices": {
      "get": {
        "operationId": "listInvoices",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["draft", "paid", "void"]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invoice list",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["items"],
                  "properties": {
                    "items": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Invoice" }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createInvoice",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/InvoiceDraft" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Invoice" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "InvoiceDraft": {
        "type": "object",
        "required": ["customerId", "amount"],
        "properties": {
          "customerId": { "type": "string" },
          "amount": { "type": "integer", "minimum": 1 },
          "note": { "type": ["string", "null"] }
        }
      },
      "Invoice": {
        "allOf": [
          { "$ref": "#/components/schemas/InvoiceDraft" },
          {
            "type": "object",
            "required": ["id", "status"],
            "properties": {
              "id": { "type": "string" },
              "status": {
                "type": "string",
                "enum": ["draft", "paid", "void"]
              }
            }
          }
        ]
      }
    }
  }
}

Сгенерировать читаемые JSON API-доки

Правильный сайт доков копирует openapi.json в статику на сборке и вешает рядом читатель. Scalar, Swagger UI, Redoc читают один файл. Не считайте «доки» ещё одной стопкой Markdown, которую потом сверяете со spec в CI. Люди правят доки, машины — spec, назавтра они расходятся.

Читатель показывает только то, что в spec: нет example — нет копируемого примера; нет ошибок — нет истории отказа; пустой description — на странице один путь. Допишите контракт, потом генерируйте доки. Превью — читатель в браузере; саму spec смотрите в JSONVue — быстрее, чем ловить оборванный $ref в огромном YAML.

URL доков можно в info.contact или внутренний портал, но не ведите вторую «таблицу полей для продукта». Перечисления и required, которые нужны продукту, уже в enum / required Schema. Две таблицы полей — классическая дверь drift.

Сгенерировать типы TypeScript: openapi-typescript

openapi-typescript — здравый дефолт: ноль рантайма, OpenAPI 3.0 и 3.1, на выходе paths и components. Одна команда: npx openapi-typescript openapi.json -o src/api/schema.d.ts. Удалённая spec тоже работает; прод-CI должна прибить файл в репозитории, чтобы сборка не била в плывущий URL.

import type { paths, components } from "./schema";

type Invoice = components["schemas"]["Invoice"];
type ListOk =
  paths["/invoices"]["get"]["responses"][200]["content"]["application/json"];

export type { Invoice, ListOk };

Индексируйте по пути, не объявляйте ещё один interface. paths["/invoices"]["get"]["responses"][200]["content"]["application/json"] — успешное тело списка. Модели — components["schemas"]["Invoice"]. Путь сменился — компилятор упадёт на вызовах, а не заставит искать три Invoice.

Типы — не проверка в рантайме. Сгенерированный .d.ts в браузере не существует. По проводу всё ещё может прийти JSON с дырками; зелёная компиляция значит лишь, что клиент написан по контракту. Для рантайм-двери сгенерируйте Zod через Orval/Kubb или проверьте ключевые ответы Schema. Не делайте «TypeScript зелёный» единственным QA.

Сгенерировать API-клиент: openapi-fetch, Orval, hey-api

Нужны только типы и вызовы — пара openapi-fetch: createClient<paths>({ baseUrl }), затем client.GET("/invoices", { params: { query } }). Тело, query и path типизированы; body createInvoice на list не уйдёт. Это тонкий дефолт фронта 2026: мало файлов, свой fetch, без склада генерации.

import createClient from "openapi-fetch";
import type { paths } from "./schema";

const client = createClient({
  baseUrl: "https://api.example.com",
  headers: { Authorization: "Bearer …" }
});

const { data, error } = await client.GET("/invoices", {
  params: { query: { status: "paid" } }
});

const created = await client.POST("/invoices", {
  body: { customerId: "cus_1", amount: 1999, note: null }
});

Нужны хуки React Query, Zod, моки MSW — Orval. Функции и хуки из той же spec — для приложений, где Query уже слой данных. hey-api (@hey-api/openapi-ts) — SDK-функции и перехватчики, когда хотите operationId как имя метода без тысяч файлов. Шаблон typescript-axios у OpenAPI Generator ещё жив; выход тяжелее, новому проекту не обязательно начинать там.

На любом конвейере не правьте сгенерированные файлы. Меняйте поведение в spec, конфиге генератора или рукописной обёртке (таймауты, ретраи, заголовок тенанта). Правка генерации — отказ от контракта. Токен в Authorization, не в JSON-теле.

Инструмент Что получаете Когда брать
openapi-typescript + openapi-fetchТипы + тонкий fetch-клиент, почти ноль рантаймаСтарт по умолчанию; кэш держите сами
OrvalТипы + функции + хуки Query + опционально Zod/MSWУже TanStack Query; моки должны идти за spec
hey-api / KubbИмена методов SDK или плагинный мультивыходПлатформа перехватчиков или один файл на операцию

Остановить drift в CI; проверить контракт в JSONVue

Контракт изменился — перегенерируйте. В CI: линт spec (Spectral или Redocly) → генератор → git diff --exit-code по дереву генерации. Есть diff — падение, чтобы автор принёс новые типы. Не гоняйте codegen раз на ноутбуке и забывайте. Бэкенд сменил поле, типы фронта с прошлой недели — стандартная авария.

npx --yes openapi-typescript openapi.json -o src/api/schema.d.ts
npx --yes @redocly/cli lint openapi.json
git diff --exit-code -- src/api/schema.d.ts

В лаборатории будет минимум три JSON: сама OpenAPI spec, тело 200, тело ошибки 422, плюс тонкая фикстура для генератора. Разные роли; не месите в одну «универсальную проверку». По одному сбою: оборванный $ref, нет operationId, в content ответа нет application/json, две spec, где сменился только enum.

Можно закончить в браузере:Форматтер JSON, чтобы увидеть, парсится ли spec;Проверка JSON Schema для components.schemas против живого body;JSON Diff, чтобы сравнить две OpenAPI или arguments модели с телом вниз по течению. Данные остаются на этой машине. Дальше: MCP и JSON Schema, структурированный вывод и оболочки инструментов из той же таблицы полей.

Рядом: MCP и JSON Schema, структурированный вывод ИИ, ошибки JSON от ИИ, что такое MCP.

FAQ

Spec в YAML или JSON?

Генераторы едят оба. В репозитории YAML добрее к человеческому diff; в браузер и JSONVue несите JSON. В CI прибейте один исходник; второй — только артефакт сборки, чтобы два источника не правили друг друга.

Можно только типы, без клиента?

Да, и часто так и надо. openapi-typescript выдаёт только .d.ts. Существующая обёртка axios может начать есть эти типы. Когда пути и тела закрыты типами, переходите на openapi-fetch или Orval. Контролируемее, чем три тысячи файлов в первый день.

Новому проекту OpenAPI 3.0 или 3.1?

Новые контракты — 3.1, чтобы сесть на JSON Schema 2020-12 и снять слой диалекта с null и const. Не форсите апгрейд парка 3.0; убедитесь, что генератор (openapi-typescript 7.x, Orval 8) объявляет 3.1, потом мигрируйте. Swagger 2.0 сначала на 3.x, потом разговор про клиент.

Могут ли OpenAPI и MCP inputSchema жить в одном документе?

Бизнес-поля могут делить одну каноническую JSON Schema, затем отдельно рождать OpenAPI requestBody и MCP inputSchema. Не отдавайте весь файл OpenAPI MCP-клиенту как каталог инструментов — транспорт, auth и имена методов другой слой. Одна таблица полей, две оболочки — меньше drift, чем копипаст.

Итог и дальше

Codegen OpenAPI — одно предложение: машиночитаемая Schema растит человеческие JSON API-доки, типы TypeScript на компиляции и типизированный клиент. Напишите любое из трёх руками — контракт перестаёт быть контрактом.

Выкатывайте в таком порядке: openapi.json парсится, есть operationId, ошибки перечислены; повесьте читатель доков; сгенерируйте типы; потом выберите клиент. Spec, успешное тело и тело ошибки оставьте в JSONVue на этой машине. Входы инструментов — MCP и JSON Schema. Ответы модели — структурированный вывод.