OpenAPI·TypeScript·Free

OpenAPI to
TypeScript.

Turn an OpenAPI 3.0 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·React·Angular·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
          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
compile
src/api/services/default.service.ts tsc cleanOutput
// src/api/services/default.service.ts
import type { ApiClient } from '../client';
import type { 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 }),
    },
  });
}
What gets generated

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';
import { WalletPayment } from './models/wallet-payment';

export type MethodUnion = CardPayment | BankPayment | WalletPayment;

// Narrowing (yours to write — the types enable it):
function label(m: MethodUnion): string {
  if (m.kind === 'card')   return `card ${m.last4}`;
  if (m.kind === 'bank')   return `bank ${m.iban}`;
  return `wallet ${m.provider}`;
}
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.

01ProofReal discriminated unions.Spec · InCode · Out
spec.yaml fragment
method:
  oneOf:
    - $ref: '#/components/schemas/CardPayment'
    - $ref: '#/components/schemas/BankPayment'
    - $ref: '#/components/schemas/WalletPayment'
  discriminator:
    propertyName: kind
emitted .ts files
// union-types.ts
export type MethodUnion =
  | CardPayment
  | BankPayment
  | WalletPayment;

// models/card-payment.ts
import { CardPaymentKind } from './card-payment-kind';

export interface CardPayment {
  kind: CardPaymentKind;
  last4: string;
}

// models/card-payment-kind.ts
export type CardPaymentKind = 'card';
React · TanStack Query

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 generated
// src/api/default.api.ts
import { useQuery, useMutation } from '@tanstack/react-query';
import { useApiClient } from '../client';
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 }) },
    });
  },
};

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 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."
See the MCP page
Questions

Things people ask.

The same seven 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, 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.

Try it on your own spec.

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

Open the converter