GraphQL to
Angular.
Turn a GraphQL SDL schema into typed Angular services. Queries and mutations as Observables over HttpClient, subscriptions over graphql-ws, inject() DI throughout. Unions keep __typename so narrowing works. One file per type, no unused-type bloat. No signup, no install.
type Query {
payment(id: ID!): Payment
payments(limit: Int): [Payment!]!
}
type Mutation {
createPayment(input: CreatePaymentInput!): Payment!
}
type Subscription {
paymentSettled: Payment!
}
enum PaymentStatus {
PENDING
SETTLED
FAILED
}
union PaymentMethod = CardPayment | BankPayment
type Payment {
id: ID!
amount: Float!
currency: String
status: PaymentStatus!
method: PaymentMethod!
}// src/api/services/query.service.ts
import { PAYMENT_QUERY, PAYMENTS_QUERY } from './query-queries';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { BaseApiService } from './base-api.service';
import { Payment } from '../models/payment';
@Injectable()
export class QueryService extends BaseApiService {
public payment(id: string): Observable<Payment | null> {
return this.executeGraphQL<Payment | null>(PAYMENT_QUERY, 'Payment', 'payment', { id });
}
public payments(limit?: number): Observable<Payment[]> {
return this.executeGraphQL<Payment[]>(PAYMENTS_QUERY, 'Payments', 'payments', { limit });
}
}
// src/api/services/subscription.service.ts
import { PAYMENT_SETTLED_QUERY } from './subscription-queries';
@Injectable()
export class SubscriptionService extends BaseApiService {
public paymentSettled(): Observable<Payment> {
return this.subscribeGraphQL<Payment>(PAYMENT_SETTLED_QUERY, 'PaymentSettled', 'paymentSettled');
}
}Idiomatic Angular. Idiomatic TypeScript.
Below: types emitted from a small payments schema with an enum and a union, plus the generated operation document. Each type lands in its own file; the operation keeps __typename on the union so the result is straightforward to narrow.
// src/api/models/payment.ts
import { PaymentMethod } from './payment-method';
import { PaymentStatus } from './payment-status';
export interface Payment {
id: string;
amount: number;
status: PaymentStatus;
method: PaymentMethod;
currency?: string | null;
}
// src/api/models/payment-method.ts
import { CardPayment } from './card-payment';
import { BankPayment } from './bank-payment';
export type PaymentMethod = CardPayment | BankPayment;
// src/api/services/query-queries.ts — the generated
// operation keeps __typename so narrowing works:
export 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
}
}
}
}`;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;
// emitted operation keeps the __typename selection
// so downstream narrowing works without casts:
//
// method {
// __typename
// ... on CardPayment { kind last4 brand }
// ... on BankPayment { kind iban }
// }Services, not string plumbing.
Every field on Query and Mutation becomes a one-liner method on QueryService or MutationService, returning a typed Observable. The operation documents — selection sets, __typename, inline fragments — are generated constants, so there is no string assembly in your code.
// src/api/services/query.service.ts
import { PAYMENT_QUERY, PAYMENTS_QUERY } from './query-queries';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { BaseApiService } from './base-api.service';
import { Payment } from '../models/payment';
@Injectable()
export class QueryService extends BaseApiService {
public payment(id: string): Observable<Payment | null> {
return this.executeGraphQL<Payment | null>(PAYMENT_QUERY, 'Payment', 'payment', { id });
}
public payments(limit?: number): Observable<Payment[]> {
return this.executeGraphQL<Payment[]>(PAYMENTS_QUERY, 'Payments', 'payments', { limit });
}
}
// src/api/services/mutation.service.ts
import { CREATE_PAYMENT_QUERY } from './mutation-queries';
import { CreatePaymentInput } from '../models/create-payment-input';
@Injectable()
export class MutationService extends BaseApiService {
public createPayment(input: CreatePaymentInput): Observable<Payment> {
return this.executeGraphQL<Payment>(CREATE_PAYMENT_QUERY, 'CreatePayment', 'createPayment', { 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 Angular 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 an Angular client
// with typed services into ./src/api."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 object type, one union type per GraphQL union, QueryService / MutationService / SubscriptionService extending a small generated BaseApiService, the operation documents as constants, and BASE_URL / GRAPHQL_WS_CLIENT injection tokens. No GraphQL client library required.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 schema.
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