Туториал
Как из 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 |
| Типы TypeScript | openapi-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. Ответы модели — структурированный вывод.