OpenAPI to
React.
Turn an OpenAPI 3.0 or 3.1 spec into a typed React client. TanStack Query hooks for every operation — useQuery for reads, useMutation for writes, stable query keys — or plain async functions when you bring your own data layer. Discriminated unions from oneOf. 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'
/payments:
post:
operationId: createPayment
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CreatePaymentRequest'
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
CreatePaymentRequest:
type: object
required: [amount, methodId]
properties:
amount: { type: number }
methodId: { type: string }
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/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),
});
}Idiomatic TypeScript. Idiomatic hooks.
Below: types emitted from a small payments spec with a oneOf discriminator. Each type lands in its own file; the union is resolved to a real TypeScript type you can narrow in JSX without casts.
// src/types/payment.ts
import { MethodUnion } from '../union-types';
export interface Payment {
id: string;
amount: number;
method: MethodUnion;
currency?: string | null;
}
// src/union-types.ts
import { CardPayment } from './types/card-payment';
import { BankPayment } from './types/bank-payment';
export type MethodUnion = CardPayment | BankPayment;
// src/types/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.
paths:
/payments/{id}:
get:
operationId: getPayment
parameters:
- name: id
in: path
required: true
- name: expand
in: queryexport function useGetPayment(id: string, expand?: string) {
const apiClient = useApiClient();
return useQuery({
queryKey: ['getPayment', id, expand],
queryFn: () => defaultApi.getPayment(apiClient, id, expand),
});
}
// Query keys include every parameter, so two
// components asking for different payments never
// share a cache entry — and the same payment
// twice never fetches twice.Hooks, not just types.
Opt into TanStack Query and every read gets useQuery, every write gets useMutation — with query keys that include each parameter, and a client pulled from context via useApiClient(). 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 })
}
});
},
};
// 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 React 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:
// "Read payments.openapi.yaml and generate a React
// client with TanStack Query hooks into ./src."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.
Things people ask.
The same 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, a union-types.ts for oneOf unions, an API object per tag with either TanStack Query hooks or plain async functions, and a small client.ts with an ApiClient, a context provider, and a useApiClient hook. No runtime generator dependency.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.