GraphQL·React·Free

GraphQL to
React hooks.

Turn a GraphQL SDL schema into a typed React client. TanStack Query hooks for every query and mutation, or plain async functions when you want to bring your own data layer. Unions keep __typename so narrowing works. One file per model, no unused-type bloat. No signup, no install.

GraphQL SDL·React·TanStack Query·MIT
schema.graphqlInput
type Query {
  payment(id: ID!): Payment
  payments(limit: Int): [Payment!]!
}

type Mutation {
  createPayment(input: CreatePaymentInput!): Payment!
}

enum PaymentStatus {
  PENDING
  SETTLED
  FAILED
}

union PaymentMethod = CardPayment | BankPayment

type Payment {
  id: ID!
  amount: Float!
  currency: String
  status: PaymentStatus!
  method: PaymentMethod!
}
compile
src/services/query-api.ts tsc cleanOutput
// src/services/query-api.ts
import { ApiClient, executeGraphQL, useApiClient } from '../client';
import { useQuery } from '@tanstack/react-query';
import { Payment } from '../models/payment';

const PAYMENT_QUERY = `query Payment($id: ID!) {
  payment(id: $id) {
    id
    amount
    currency
    status
    method {
      __typename
      ... on CardPayment { kind last4 brand }
      ... on BankPayment { kind iban }
    }
  }
}`;

export const queryApi = {
  payment: async (apiClient: ApiClient, id: string): Promise<Payment | null> => {
    return executeGraphQL<Payment | null>(
      apiClient, PAYMENT_QUERY, 'Payment', 'payment', { 'id': id });
  },
};

export function usePayment(id: string) {
  const apiClient = useApiClient();
  return useQuery({
    queryKey: ['payment', id],
    queryFn: () => queryApi.payment(apiClient, id),
  });
}
What gets generated

Idiomatic TypeScript. Idiomatic hooks.

Below: types emitted from a small payments schema with an enum, a union and an input. Each type lands in its own file; the operation keeps __typename on the union so the result type is straightforward to narrow.

// src/models/payment.ts
import { PaymentStatus } from './payment-status';

export interface Payment {
  id: string;
  amount: number;
  status: PaymentStatus;
  method: string;
  currency?: string | null;
}

// src/models/payment-status.ts
export type PaymentStatus = 'PENDING' | 'SETTLED' | 'FAILED';

// src/models/payment-method.ts
import { CardPayment } from './card-payment';
import { BankPayment } from './bank-payment';

export type PaymentMethod = CardPayment | BankPayment;

// src/models/card-payment.ts
export interface CardPayment {
  readonly __typename: 'CardPayment';
  kind: string;
  last4: string;
  brand: string;
}
Three things we get right

Grounded in how the schema reads.

Each claim here is something you can verify by running the converter on your own schema. Click a card to see the schema fragment that triggered it and the code that came out.

01ProofUnions keep their shape.Spec · InCode · Out
schema.graphql fragment
union PaymentMethod = CardPayment | BankPayment

type CardPayment {
  kind: String!
  last4: String!
  brand: String!
}

type BankPayment {
  kind: String!
  iban: String!
}
emitted .ts files
// models/payment-method.ts
import { CardPayment } from './card-payment';
import { BankPayment } from './bank-payment';

export type PaymentMethod = CardPayment | BankPayment;

// models/card-payment.ts
export interface CardPayment {
  readonly __typename: 'CardPayment';
  kind: string;
  last4: string;
  brand: string;
}

// emitted operation keeps the __typename selection
// so downstream narrowing works without casts:
//
// ... on CardPayment { kind last4 brand }
// ... on BankPayment { kind iban }
React · TanStack Query

Hooks, not just types.

Opt into TanStack Query and every query gets a useQuery hook, every mutation a useMutation. Query keys are stable per variable. The ApiClient is resolved from context, so tests and multi-tenant apps swap it without touching call sites.

src/services/query-api.ts generated
// src/services/query-api.ts
import { ApiClient, executeGraphQL, useApiClient } from '../client';
import { useQuery } from '@tanstack/react-query';
import { Payment } from '../models/payment';

const PAYMENT_QUERY = `query Payment($id: ID!) {
  payment(id: $id) {
    id
    amount
    currency
    status
  }
}`;

export const queryApi = {
  payment: async (apiClient: ApiClient, id: string): Promise<Payment | null> => {
    return executeGraphQL<Payment | null>(
      apiClient, PAYMENT_QUERY, 'Payment', 'payment', { 'id': id });
  },
};

export function usePayment(id: string) {
  const apiClient = useApiClient();
  return useQuery({
    queryKey: ['payment', id],
    queryFn: () => queryApi.payment(apiClient, id),
  });
}

// src/services/mutation-api.ts
export function useCreatePaymentMutation() {
  const apiClient = useApiClient();
  return useMutation({
    mutationFn: (input: CreatePaymentInput) =>
      mutationApi.createPayment(apiClient, input),
  });
}
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 GraphQL schema 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 schema.graphql and generate a React client
//  with TanStack Query hooks into ./src."
See the MCP page
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 object type, one string-literal union per enum, and either TanStack Query hooks or plain async functions per operation. Plus a small client.ts with an ApiClient, a context provider, and a useApiClient hook. No runtime generator dependency.

Try it on your own schema.

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

Open the converter