Tutorial

JSON-API-Docs, TypeScript-Typen und API-Client aus OpenAPI erzeugen: vom Schema zum Codegen

Hand-Docs, Hand-Typen und ein Hand-Fetch, die je für sich driften, kosten mehr als der Endpoint. Behandeln Sie das OpenAPI-Schema als Vertrag, damit Docs, Typen und Client aus demselben JSON wachsen.

Das Teure an GET /invoices ist selten der Handler. Es sind drei Artefakte, die je für sich veralten: menschliche API-Docs, TypeScript-Typen für den Compiler, ein Fetch-Wrapper zur Laufzeit. Teams kleben noch Beispiel-JSON ins Wiki, schreiben dieselben Felder in types.ts und konkatenieren URLs in api.ts. Ein OpenAPI-Schema faltet das zu einem maschinenlesbaren Vertrag. Sie pflegen openapi.json (oder YAML); Doc-Site, Typdatei und Client werden daraus erzeugt. Ein Feld ändert sich, alle drei bewegen sich. MCP und JSON Schema behandelt Tool-Eingaben; Structured Output die finale Modellantwort. Hier nur die HTTP-JSON-API: Schema zu Docs, Typen, Client. Spez: OpenAPI 3.1. Standardgenerator: openapi-typescript.

Ein Schema, drei Produkte: Docs, Typen, Client

Nageln Sie das Ziel fest. OpenAPI-Docs sind kein zweites Markdown-Buch; sie rendern dieselbe Spec. Scalar, Swagger UI, Redoc sind Leser. TypeScript-Typen sind keine zweite handgeschriebene Schnittstelle; sie projizieren paths und components.schemas. Ein API-Client ist nicht „noch ein Axios-Wrapper“; es sind Funktionen aus operationId oder Pfaden, mit Request- und Response-Typen. Sind zwei von dreien handgeschrieben, ist das Schema ein Poster.

Werkzeuge später; Vertrag zuerst. Exportieren Sie eine Spec aus FastAPI, Nest, Spring oder Go, oder schreiben Sie openapi.json und implementieren dagegen. Verboten: zuerst den Client schreiben und Docs nach dem „Gefühl“ der Aufrufer erfinden. Keine einzige Wahrheit, und die CI kann nicht entscheiden, wer recht hat. Nutzt ein Agent REST als Werkzeug, soll er dieses OpenAPI lesen, nicht ein zweites MCP-inputSchema — zwei Verträge driften sofort. Schichten: der MCP-Artikel oben.

OpenAPI 3.1 rückt components.schemas an JSON Schema 2020-12: nullable ist type: ["string", "null"], nicht mehr nullable: true aus 3.0. Weniger „Docs optional, Typen Pflicht“. Das Objekt in JSONVue soll das sein, das der Generator frisst — kein Dialekt beim Export.

Produkt Woher es kommt Nicht von Hand schreiben
JSON-API-DocsDasselbe OpenAPI-JSON, gerendert von Scalar / Swagger UIBeispiel-JSON und Screenshots im Wiki
TypeScript-Typenopenapi-typescript erzeugt paths / componentsEin paralleles interface Invoice
API-Client / Hooksopenapi-fetch, Orval oder hey-api erzeugen AufruffunktionenZusammengebaute URLs und as Invoice

Zuerst das OpenAPI-JSON richtig schreiben: paths, components, operationId

Ein Generator überholt die Spec-Qualität nicht. Minimum: openapi-Version, info, paths, geteilte Modelle in components.schemas. Jede Operation bekommt eine stabile operationId (listInvoices, createInvoice); der Client nutzt sie als Funktionsnamen. Ändert sich ein Pfad, die Id nicht mitschieben. application/json an Request und Erfolg, Modelle per $ref, keine Feldtabelle auf jedem Path.

Fehlerantworten gehören in den Vertrag. Geben Sie 400 / 401 / 404 / 422 JSON Schemas, damit der Client eine lesbare Union bildet, kein unknown. Query- und Path-Parameter in parameters; nicht im Body verstecken und in den Docs mündlich vereinbaren. Bearer/OIDC in securitySchemes, damit der generierte Client weiß, wohin das Token gehört. Auth-Protokoll: OAuth 2.1 — auch bei REST lebt das Token im Header, nicht im JSON-Umschlag.

Unten eine minimale Rechnungs-API. Kein Produktspec, aber genug, um Docs, Typen und Client gleichzeitig zu drehen. Invoice erscheint einmal; list und create referenzieren sie. Lokal formatieren, Parse prüfen, dann $ref auflösen.

{
  "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"]
              }
            }
          }
        ]
      }
    }
  }
}

Lesbare JSON-API-Docs erzeugen

Die richtige Doc-Site kopiert openapi.json beim Build ins statische Verzeichnis und hängt einen Leser daneben. Scalar, Swagger UI, Redoc lesen dieselbe Datei. Behandeln Sie „Docs“ nicht als weiteren Markdown-Stapel, den Sie in der CI gegen die Spec halten. Menschen editieren Docs, Maschinen die Spec — am nächsten Tag forken sie.

Ein Leser zeigt nur, was in der Spec steht: kein example, kein kopierbares Sample; keine Fehlerantworten, keine Failure-Story; leere description, nur ein Pfad. Vertrag fertig, dann Docs erzeugen. Reader im Browser öffnen; die Spec selbst in JSONVue — schneller als ein gebrochenes $ref in riesigem YAML.

Doc-URL in info.contact oder ein internes Portal, aber keine zweite „Feldtabelle für Produkt“. Enums und Required, die Produkt interessieren, stehen schon in Schema-enum / required. Zwei Feldtabellen sind die klassische Drift-Tür.

TypeScript-Typen erzeugen: openapi-typescript

openapi-typescript ist ein solides Default: null Runtime, OpenAPI 3.0 und 3.1, paths und components. Ein Befehl: npx openapi-typescript openapi.json -o src/api/schema.d.ts. Remote-Specs gehen; Produktions-CI soll eine Datei im Repo pinnen, damit der Build keine wandernde URL trifft.

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

Nach Pfad indexieren; kein weiteres interface. paths["/invoices"]["get"]["responses"][200]["content"]["application/json"] ist der List-Erfolgsbody. Modelle über components["schemas"]["Invoice"]. Ändert sich ein Pfad, scheitert der Compiler an den Call-Sites, statt dass Sie drei Invoice suchen.

Typen sind keine Laufzeitprüfung. Die erzeugte .d.ts existiert nicht im Browser. Die Leitung kann weiter JSON mit fehlenden Feldern liefern; grünes Compile heißt nur: Sie haben den Client gegen den Vertrag geschrieben. Für ein Runtime-Tor Zod mit Orval/Kubb erzeugen oder kritische Responses per Schema prüfen. „TypeScript ist grün“ ist nicht die einzige QA.

API-Client erzeugen: openapi-fetch, Orval, hey-api

Nur Typen plus Aufrufe: openapi-fetch koppeln. createClient<paths>({ baseUrl }), dann client.GET("/invoices", { params: { query } }). Body, Query, Path sind typisiert; Sie schicken keinen createInvoice-Body an list. Das ist das dünne Frontend-Default 2026: wenige Dateien, eigenes Fetch, kein generiertes Lager.

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-Hooks, Zod, MSW-Mocks: Orval. Funktionen und Hooks aus derselben Spec — passt zu Apps, die Query schon als Datenschicht nutzen. hey-api (@hey-api/openapi-ts): SDK-Funktionen plus Interceptors, wenn Sie operationId als Methodennamen wollen, ohne Tausende Dateien. Das typescript-axios-Template von OpenAPI Generator läuft noch; die Ausgabe ist schwerer, neues Arbeit muss dort nicht starten.

Auf jeder Pipeline: generierte Dateien nicht von Hand ändern. Verhalten in der Spec, der Generator-Config oder einem handgeschriebenen Wrapper (Timeouts, Retries, Tenant-Header). Generiertes editieren gibt den Vertrag auf. Token auf Authorization, nicht im JSON-Body.

Werkzeug Was Sie bekommen Wann wählen
openapi-typescript + openapi-fetchTypen + dünner Fetch-Client, fast null RuntimeDefault-Start; Sie halten den Cache
OrvalTypen + Funktionen + Query-Hooks + optionales Zod/MSWSie nutzen schon TanStack Query; Mocks sollen der Spec folgen
hey-api / KubbSDK-Methodennamen oder Plugin-MehrfachausgabeInterceptor-Plattform oder eine Datei pro Operation

Drift in der CI stoppen; Vertrag in JSONVue prüfen

Vertrag ändert sich: neu erzeugen. In der CI: Spec linten (Spectral oder Redocly) → Generator → git diff --exit-code auf dem generierten Baum. Diff = Fail, damit neue Typen committed werden. Codegen nicht einmal auf dem Laptop laufen und vergessen. Backend änderte ein Feld, Frontend-Typen sind von letzter Woche — der Standardunfall.

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

Ein Lab liefert mindestens drei JSON: die OpenAPI-Spec, einen 200-Body, einen 422-Fehlerbody, plus eine schlanke Fixture für den Generator. Unterschiedliche Jobs; nicht in eine „generische Validierung“ kneten. Je ein Fehlersample: gebrochenes $ref, fehlende operationId, Response ohne application/json, zwei Specs die nur ein Enum änderten.

Im Browser fertig:JSON-Formatierer, um zu sehen ob die Spec parst;JSON-Schema-Validator für components.schemas gegen einen echten Body;JSON Diff, um zwei OpenAPI-Versionen oder Modell-arguments gegen Downstream-Body zu vergleichen. Daten bleiben hier. Weiter: MCP und JSON Schema, Structured Output, Tool-Schalen aus derselben Feldtabelle.

Verwandt: MCP und JSON Schema, KI Structured Output, KI-JSON-Fehler, Was ist MCP.

FAQ

YAML oder JSON für die Spec?

Generatoren fressen beides. YAML ist freundlicher für menschliche Diffs; JSON an Browser und JSONVue. Eine Quelldatei in der CI pinnen; die andere nur Build-Artefakt, damit zwei Quellen sich nicht gegenseitig editieren.

Nur Typen, keinen Client?

Ja, und oft soll das so sein. openapi-typescript emittiert nur .d.ts. Ein bestehender Axios-Wrapper kann diese Typen schrittweise fressen. Wenn Pfade und Bodies von Typen umzäunt sind, wechseln Sie zu openapi-fetch oder Orval. Kontrollierbarer als dreitausend Dateien am ersten Tag.

OpenAPI 3.0 oder 3.1 für ein neues Projekt?

Neue Verträge: 3.1, damit JSON Schema 2020-12 und weniger Dialekt bei Null und const. Kein Zwangsupgrade eines 3.0-Bestands; erst prüfen, dass der Generator (openapi-typescript 7.x, Orval 8) 3.1 dokumentiert, dann wandern. Swagger 2.0 zuerst auf 3.x, dann über den Client reden.

Können OpenAPI und MCP-inputSchema ein Dokument teilen?

Geschäftsfelder können ein kanonisches JSON Schema teilen, dann OpenAPI-requestBody und MCP-inputSchema getrennt erzeugen. Geben Sie einem MCP-Client nicht die ganze OpenAPI-Datei als Tool-Katalog — Transport, Auth und Methodennamen sind andere Schichten. Eine Feldtabelle, zwei Schalen, driftet weniger als Copy-Paste.

Fazit und nächste Schritte

OpenAPI-Codegen ist ein Satz: ein maschinenlesbares Schema wächst menschliche JSON-API-Docs, Compile-Time-TypeScript-Typen und einen typisierten Client. Schreiben Sie eines von Hand, ist der Vertrag keiner mehr.

In dieser Reihenfolge: openapi.json parst, hat operationId, listet Fehler; Reader aufhängen; Typen erzeugen; dann Client wählen. Spec, Erfolgsbody und Fehlerbody in JSONVue auf dieser Maschine. Tool-Eingaben: MCP und JSON Schema. Modellantworten: Structured Output.