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.
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!
}// 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),
});
}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;
}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.
union PaymentMethod = CardPayment | BankPayment
type CardPayment {
kind: String!
last4: String!
brand: String!
}
type BankPayment {
kind: String!
iban: String!
}// 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 }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
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),
});
}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."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 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.