OpenAPI·React·Free

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.1·React 18+·TanStack Query·Fetch·MIT
payments.openapi.yamlInput
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 }
compile
src/api/default.api.ts tsc cleanOutput
// 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),
  });
}
What gets generated

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}`;
}
Three things we get right

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.

01ProofHooks you would have written.Spec · InCode · Out
spec.yaml fragment
paths:
  /payments/{id}:
    get:
      operationId: getPayment
      parameters:
        - name: id
          in: path
          required: true
        - name: expand
          in: query
emitted .ts
export 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.
React · TanStack Query

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 generated
// 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),
  });
}
Also available via MCP

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."
See the MCP page
Prefer it in your build?

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.

Questions

Things people ask.

The same questions we keep getting. If yours is not here, run the converter — the answer is usually in the output.

A tree of .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.
More converters

Same engine, other stacks.

Every converter runs the same spec-to-IR pipeline. Pick another source format or target stack.

OpenAPITypeScript
Angular httpResource, React TanStack Query, or framework-agnostic fetch.
Open converter
OpenAPIAngular
Typed services with httpResource, Signals, inject() DI, and interceptors.
Open converter
OpenAPIFetch
Framework-agnostic fetch client with ApiResult, middleware, and retries.
Open converter
OpenAPIJava Spring
RestClient services, Java records, sealed interfaces, Spring Boot 3.
Open converter
OpenAPIPython
Async httpx clients paired with Pydantic v2 models.
Open converter
OpenAPIGo
Idiomatic net/http client, context-aware, pointer-nullable fields.
Open converter
OpenAPIKotlin
Ktor client, kotlinx.serialization, sealed interfaces for oneOf.
Open converter
OpenAPIC#
Records, nullable reference types, JsonPolymorphic, HttpClient.
Open converter
OpenAPIRust
reqwest client with serde-tagged enums for oneOf and Option<T>.
Open converter
GraphQLAngular
Observable services with typed queries, mutations, and graphql-ws subscriptions.
Open converter
GraphQLReact
TanStack Query hooks with discriminated-union types and query keys.
Open converter
GraphQLKotlin
Ktor, kotlinx.serialization, Flow-based subscriptions.
Open converter
GraphQLC#
Records, nullable types, [JsonPolymorphic] unions, IAsyncEnumerable subscriptions.
Open converter
ProtobufKotlin
Ktor, coroutines, and sealed classes straight from .proto.
Open converter
ProtobufC#
Records, nullable types, Connect RPC over HttpClient.
Open converter
SQLTypeScript
Postgres DDL to typed interfaces with foreign-key navigation.
Open converter
SQLKotlin
CREATE TABLE scripts to Kotlin @Serializable data classes.
Open converter

Try it on your own spec.

The converter runs in the browser. Your spec never leaves the page.

Open the converter