OpenAPI to
Angular.
Turn an OpenAPI 3.0 or 3.1 spec into typed Angular services. httpResource() with Signals for reactive reads, Observable methods for everything else, inject() DI, and functional interceptors for auth and retries. Discriminated unions from oneOf. One file per model. No signup, no install.
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'
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
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 }// src/api/services/default.service.ts
import { Injectable, Signal } from '@angular/core';
import { Observable } from 'rxjs';
import { HttpResourceRef } from '@angular/common/http';
import { BaseApiService } from './base-api.service';
import { Payment } from '../models/payment';
@Injectable()
export class DefaultService extends BaseApiService {
public getPayment(id: string, expand?: string): Observable<Payment> {
const paramObj: Record<string, unknown> = {};
if (typeof expand !== 'undefined') paramObj['expand'] = expand;
return this.execute<Payment>(`/payments/${id}`, undefined,
{ method: 'get', params: this.createParams(paramObj) });
}
public getPaymentResource(
id: Signal<string | undefined>,
expand?: Signal<string | undefined>,
): HttpResourceRef<Payment | undefined> {
return this.buildResource<Payment>(() => {
const idValue = id();
const expandValue = expand?.();
if (typeof idValue === 'undefined') {
return undefined;
}
const paramObj: Record<string, unknown> = {};
if (typeof expandValue !== 'undefined') paramObj['expand'] = expandValue;
const _queryString = this.createParams(paramObj).toString();
return this.buildUrl(`/payments/${idValue}${_queryString ? '?' + _queryString : ''}`);
});
}
}Idiomatic Angular. Idiomatic TypeScript.
Below: models emitted from a small payments spec with a oneOf discriminator. Each model lands in its own file, the union is a real TypeScript type, and the base URL arrives through an injection token — DI from the first line.
// 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';
export type MethodUnion = CardPayment | BankPayment;
// src/api/models/card-payment.ts
export interface CardPayment {
kind: 'card';
last4: string;
}
// src/api/injection-token.ts
import { InjectionToken } from '@angular/core';
export const BASE_URL = new InjectionToken<string>('BASE_URL');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.
paths:
/payments/{id}:
get:
operationId: getPayment
parameters:
- name: id
in: path
required: true
schema: { type: string }public getPaymentResource(
id: Signal<string | undefined>,
): HttpResourceRef<Payment | undefined> {
return this.buildResource<Payment>(() => {
const idValue = id();
if (typeof idValue === 'undefined') {
return undefined;
}
return this.buildUrl(`/payments/${idValue}`);
});
}
// In a component: pass a Signal, read a Signal.
// The resource re-fetches when the input changes
// and stays idle while the id is undefined.Resources, not just types.
GET operations emit httpResource() methods: pass Signals, get an HttpResourceRef with value, status and error as Signals. The resource tracks its inputs, re-fetches on change, and an optional trigger signal defers loading until you ask.
// src/api/services/default.service.ts
import { Injectable, Signal } from '@angular/core';
import { HttpResourceRef } from '@angular/common/http';
import { BaseApiService } from './base-api.service';
import { Payment } from '../models/payment';
@Injectable()
export class DefaultService extends BaseApiService {
public getPaymentResource(
id: Signal<string | undefined>,
expand?: Signal<string | undefined>,
): HttpResourceRef<Payment | undefined> {
return this.buildResource<Payment>(() => {
const idValue = id();
const expandValue = expand?.();
if (typeof idValue === 'undefined') {
return undefined;
}
const paramObj: Record<string, unknown> = {};
if (typeof expandValue !== 'undefined') paramObj['expand'] = expandValue;
const _queryString = this.createParams(paramObj).toString();
return this.buildUrl(`/payments/${idValue}${_queryString ? '?' + _queryString : ''}`);
});
}
}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 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 payments.openapi.yaml and generate an Angular
// client with httpResource 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 injectable service per tag extending a small generated BaseApiService, one interface per schema, a union-types.ts for oneOf unions, and a BASE_URL injection token. GET operations additionally emit httpResource() methods when enabled. No runtime dependency beyond Angular itself.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 spec.
The converter runs in the browser. Your spec never leaves the page.