OpenAPI to
TypeScript.
Turn an OpenAPI 3.0 or 3.1 spec into a typed TypeScript client. React with TanStack Query, Angular with httpResource, or plain Fetch. Discriminated unions from oneOf. One file per model so bundlers tree-shake cleanly. No signup, no install.
openapi: 3.0.3
info:
title: Payments API
version: 1.2.0
paths:
/payments/{id}:
get:
operationId: getPayment
parameters:
- name: id
in: path
required: true
schema: { type: string }
- name: expand
in: query
schema: { type: string }
responses:
'200':
content:
application/json:
schema:
$ref: '#/components/schemas/Payment'
components:
schemas:
Payment:
type: object
required: [id, amount, method]
properties:
id: { type: string }
amount: { type: number }
currency:
type: string
nullable: true
method:
oneOf:
- $ref: '#/components/schemas/CardPayment'
- $ref: '#/components/schemas/BankPayment'
discriminator:
propertyName: kind
CardPayment:
type: object
required: [kind, last4]
properties:
kind: { type: string, enum: [card] }
last4: { type: string }
BankPayment:
type: object
required: [kind, iban]
properties:
kind: { type: string, enum: [bank] }
iban: { type: string }// src/api/services/default.service.ts
import { ApiClient } from '../client';
import { Payment } from '../models/payment';
export async function getPayment(
apiClient: ApiClient,
id: string,
expand?: string,
): Promise<Payment> {
return apiClient.request<Payment>({
method: 'GET',
url: `/payments/${id}`,
query: {
...(expand !== undefined && { 'expand': expand }),
},
});
}Idiomatic TypeScript. No generator scaffolding.
Below: types emitted from a small payments spec with a oneOf discriminator. Each model lands in its own file; the union is resolved to a real TypeScript type.
// src/api/models/payment.ts
import { MethodUnion } from '../union-types';
export interface Payment {
id: string;
amount: number;
method: MethodUnion;
currency?: string | null;
}
// src/api/union-types.ts
import { CardPayment } from './models/card-payment';
import { BankPayment } from './models/bank-payment';
export type MethodUnion = CardPayment | BankPayment;
// src/api/models/card-payment.ts
export interface CardPayment {
kind: 'card';
last4: string;
}
// Narrowing (yours to write — the types enable it):
function label(m: MethodUnion): string {
if (m.kind === 'card') return `card ${m.last4}`;
return `bank ${m.iban}`;
}Grounded in how the spec reads.
Each claim here is something you can verify by running the converter on your own spec. Click a card to see the spec fragment that triggered it and the code that came out.
method:
oneOf:
- $ref: '#/components/schemas/CardPayment'
- $ref: '#/components/schemas/BankPayment'
discriminator:
propertyName: kind// union-types.ts
import { CardPayment } from './models/card-payment';
import { BankPayment } from './models/bank-payment';
export type MethodUnion = CardPayment | BankPayment;
// models/card-payment.ts
export interface CardPayment {
kind: 'card';
last4: string;
}
// models/bank-payment.ts
export interface BankPayment {
kind: 'bank';
iban: string;
}Hooks, not just types.
Opt in to TanStack Query and the generator emits useQuery for reads and useMutation for writes — with stable query keys, client-via-context, and nothing you would not ship.
// src/api/default.api.ts
import { ApiClient, useApiClient } from '../client';
import { useQuery, useMutation } from '@tanstack/react-query';
import { Payment } from '../types/payment';
import { CreatePaymentRequest } from '../types/create-payment-request';
export const defaultApi = {
getPayment: async (apiClient: ApiClient, id: string, expand?: string): Promise<Payment> => {
return apiClient.request<Payment>({
method: 'GET',
url: `/payments/${id}`,
query: {
...(expand !== undefined && { 'expand': expand })
}
});
},
createPayment: async (apiClient: ApiClient, body?: CreatePaymentRequest): Promise<Payment> => {
return apiClient.request<Payment>({
method: 'POST',
url: '/payments',
body: body
});
}
};
// TanStack Query Hooks
export function useGetPayment(id: string, expand?: string) {
const apiClient = useApiClient();
return useQuery({
queryKey: ['getPayment', id, expand],
queryFn: () => defaultApi.getPayment(apiClient, id, expand),
});
}
export function useCreatePaymentMutation() {
const apiClient = useApiClient();
return useMutation({
mutationFn: (body?: CreatePaymentRequest) => defaultApi.createPayment(apiClient, body),
});
}Or ask Claude to do it.
The same generator ships as a Model Context Protocol server. It calls the same web API this converter uses, so the output is identical. Point Claude Desktop, Cursor, or any MCP-aware agent at a spec and it produces the TypeScript client as a diff — free, no token, no leaving the chat.
// claude-desktop config · .mcp.json
{
"mcpServers": {
"metaengine": {
"command": "npx",
"args": ["-y", "@metaengine/mcp-server"]
}
}
}
// Then in Claude:
// "Load specs/billing.v3.yaml and generate a
// TypeScript client with TanStack Query hooks."Same generator, as a package.
The converter above is the fastest way to try the output. For CI and repeatable builds, the identical generator ships as a package — pin a version, wire it into your pipeline, and review diffs like any other code.
@metaengine/openapi-angularhttpResource, Signals and inject() DI as an npm CLI — generate into your repo and pin versions.Package docs & options@metaengine/openapi-reactTanStack Query hooks as an npm CLI — generate into your repo and review diffs in CI.Package docs & options@metaengine/openapi-fetchZero-dependency Fetch client as an npm CLI — runs on Node, browsers, Vite, and Next.js.Package docs & optionsThings people ask.
The same seven questions we keep getting. If yours is not here, run the converter — the answer is usually in the output.
.ts files: one interface per schema, one enum per enum, and either a React hooks module, an Angular service, or plain Fetch functions — whichever target you selected. No runtime, no client class you did not ask for.Same engine, other stacks.
Every converter runs the same spec-to-IR pipeline. Pick another source format or target stack.
Try it on your own spec.
The converter runs in the browser. Your spec never leaves the page.