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 UI runs in your browser. When you generate, your specification (or its URL), selected options, and any custom content you explicitly include are sent over HTTPS to the MetaEngine API. The API processes the payload ephemerally; specifications and generated code are not persisted or logged.
Read the public privacy policy