Requires Angular 20+ and Nx 20+ — verified in CI, see Compatibility. The generated code uses
httpResource().
An Angular 22 · Nx monorepo that demonstrates tree-shakeable, signal-native API clients generated from OpenAPI 3.x specs.
The core idea: one InjectionToken per API endpoint, each in its own .ts file.
Because esbuild tree-shakes at file boundaries, any token you never inject() costs zero bytes in your bundle.
Why this one?
- Resources, not services. Each endpoint is an
InjectionTokenwhose function returns a signal-nativehttpResource(or, per endpoint, anObservableviaHttpClient). There is no service class to inject whole, so unused endpoints tree-shake away. - Types come from
openapi-typescript, not from generated model classes. - Testing is part of the package: a mock bus, a
/testingentry point, optional MSW handlers and a Chrome DevTools panel to catch, answer and replay requests.
An honest summary. All of these are good tools; pick by what you need.
| This project | ng-openapi-gen | Orval | Hey API | |
|---|---|---|---|---|
| Output shape | One token + function per endpoint | Service classes + model classes | Functions or services, per config | SDK functions / classes, plugins |
httpResource |
Yes (default) | Declined upstream | Yes (httpClient, httpResource or both) |
Yes |
HttpClient / Observable |
Yes, per endpoint | Yes | Yes | Yes |
| Runs as | Nx generator | Standalone CLI | Standalone CLI | Standalone CLI |
| Mock data / Zod / validators | Mock bus, MSW handlers, response validation (opt-in) | No | MSW + Faker, Zod (including request bodies) | Faker, MSW, validator plugins |
| Debug UI | Chrome DevTools panel | No | No | No |
Where the others are the better fit: if you are not on Nx, use a standalone CLI (Orval or Hey API) for now; if you need Zod schemas for request bodies or generated fake data, Orval and Hey API have them and we do not; if you want a non-Angular client, this is not the tool. (Comparison read in October 2026; tools change, so check their docs.)
Bundle size: the one-file-per-endpoint design tree-shakes cleanly (8 endpoints cost about 0.2 kB more than 1), but the default httpResource flavour carries Angular's own httpResource runtime (about +7 kB gzip). The benchmark has the numbers and a script to reproduce them.
Coming from another tool? See the migration guides: from ng-openapi-gen · from Orval.
| Path | Type | Description |
|---|---|---|
tools/openapi-resource-gen/ |
Nx generator · npm package | Reads an OpenAPI spec, emits one token file per endpoint |
tools/openapi-resource-mocks/ |
npm package | Zero-HTTP mock bus for generated tokens — Playwright E2E + Chrome Extension integration |
tools/openapi-resource-devtools/ |
Chrome Extension shell | Manifest, content script, service worker, devtools page |
apps/devtools-panel/ |
Angular 22 app | Panel UI bundled inside the Chrome Extension |
libs/github-data-access/ |
Generated data-access lib | GitHub REST API (~38 endpoints used) |
libs/petstore-data-access/ |
Generated data-access lib | OAI Petstore v3 (12 endpoints) |
libs/weather-data-access/ |
Generated data-access lib | Open-Meteo forecast API |
libs/youtube-data-access/ |
Generated data-access lib | YouTube Data API v3 (76 endpoints) |
apps/api-explorer/ |
Angular 22 app | Demo app that consumes all data-access libs |
Published on npm: @constantant/openapi-resource-gen
Step 1 — install the generator (once per workspace):
npm install -D @constantant/openapi-resource-genStep 2 — generate a data-access lib from any OpenAPI 3.x spec (local file or URL):
npx nx g @constantant/openapi-resource-gen:api-resource \
--specPath=https://petstore3.swagger.io/api/v3/openapi.yaml \
--outputDir=libs/petstore-data-access/src \
--baseUrlToken=PETSTORE_BASE_URLStep 3 — wire up providers and inject in your component:
// app.config.ts
import { provideHttpClient } from '@angular/common/http';
import { PETSTORE_BASE_URL, provideFindPetsByStatus } from './libs/petstore-data-access/src';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
{ provide: PETSTORE_BASE_URL, useValue: 'https://petstore3.swagger.io/api/v3' },
provideFindPetsByStatus(),
],
};// pets-page.component.ts
@Component({ ... })
export class PetsPageComponent {
private findPetsByStatus = inject(FIND_PETS_BY_STATUS);
readonly status = signal<'available' | 'pending' | 'sold'>('available');
readonly pets = this.findPetsByStatus(() => ({ status: this.status() }));
}Re-run the generator command whenever your spec changes — it overwrites generated files and removes any that no longer exist in the spec.
| Option | Required | Default | Description |
|---|---|---|---|
specPath |
yes | — | Path or https:// URL to the OpenAPI 3.x YAML or JSON spec file |
outputDir |
yes | — | Output directory for generated files (relative to workspace root) |
baseUrlToken |
no | API_BASE_URL |
Name of the InjectionToken holding the API base URL |
tagFilter |
no | — | Comma-separated list of OpenAPI tags to include (empty = all tags) |
namingConvention |
no | kebab |
File naming convention: kebab-case filenames with SCREAMING_SNAKE token names |
providedIn |
no | none |
none: token has no factory; a provide{Name}() helper is exported for explicit scoped provision. root: token self-registers at root (tree-shakeable singleton, no multi-context support) |
includeMocks |
no | false |
Generate a .mock.ts file per endpoint (and index.mock.ts barrels) using @constantant/openapi-resource-mocks. Requires the package to be installed. |
specId |
no | — | Identifier for this API spec, embedded in MockResourceMeta and mocks.manifest.json. Defaults to baseUrlToken lowercased with _BASE_URL stripped (e.g. PETSTORE_BASE_URL → petstore). |
verbose |
no | false |
Print a summary of created, updated, and deleted files after generation. |
dateType |
no | string |
Convert format:date-time / format:date response fields to Date or Temporal objects. Emits a typed XxxRevived alias and reviveXxxDates() helper per affected endpoint. |
readWriteMarkers |
no | false |
Honor readOnly / writeOnly in the spec: request body types (XxxBody) drop readOnly properties such as server-generated ids, and response types (XxxResponse, XxxError) drop writeOnly ones such as passwords. Uses openapi-typescript's readWriteMarkers, which adds the Readable / Writable helpers to schema.d.ts. |
readonlyResponses |
no | false |
Wrap all XxxResponse and XxxError type aliases in Readonly<> to prevent accidental mutation of response data. |
includeMswHandlers |
no | false |
Generate a .msw.ts file per endpoint (and index.msw.ts barrels) with MSW 2.x http.* handlers. Requires msw to be installed. |
clientType |
no | httpResource |
HTTP primitive wrapped by generated tokens. httpClient: the token yields a function returning a cold Observable via HttpClient (params/body are plain values, no thunks or signals). |
reportProgress |
no | false |
Emit reportProgress: true on httpResource tokens that upload a binary/multipart body or download a blob, so httpResource().progress() reports transfer progress. Has no effect on httpClient endpoints. |
callOptions |
no | false |
Accept a trailing per-call options argument on every token function: HttpContext, headers, withCredentials and the other request fields Angular supports, plus (for httpResource tokens) defaultValue, equal, injector and debugName. A defaultValue narrows the returned resource's value to non-undefined. Emits a shared request-options.ts. |
convertSwagger2 |
no | true |
Convert Swagger 2.0 specs to OpenAPI 3.0 in memory (using @scalar/openapi-upgrader) before generating. Set to false to reject Swagger 2.0 specs instead. |
httpClientTags |
no | — | Comma-separated OpenAPI tags whose endpoints use HttpClient, overriding clientType. Errors if a tag matches no endpoint. |
httpClientOperations |
no | — | Comma-separated operationIds that use HttpClient, overriding clientType. Errors if an operationId matches no endpoint. |
validateResponses |
no | false |
Validate JSON responses at runtime against the spec's response schema via httpResource's parse hook, throwing on mismatch. Requires @cfworker/json-schema to be installed. Endpoints whose response schema has a circular $ref are skipped. |
See tools/openapi-resource-gen/README.md for full documentation, or the step-by-step tutorials.
Every endpoint becomes a typed InjectionToken whose factory returns an httpResource.
For GET endpoints with query params, the reactive lambda uses a block-body form so it can
return undefined to suppress the request when a thunk-based params arg returns undefined:
// libs/petstore-data-access/src/pet/find-pets-by-status.token.ts
import { InjectionToken, inject, FactoryProvider } from '@angular/core';
import { httpResource } from '@angular/common/http';
import type { paths } from '../schema.d';
import { PETSTORE_BASE_URL } from '../api-base-url.token';
export type FindPetsByStatusParams =
paths['/pet/findByStatus']['get']['parameters']['query'];
export type FindPetsByStatusResponse =
paths['/pet/findByStatus']['get']['responses']['200']['content']['application/json'];
export const FIND_PETS_BY_STATUS = new InjectionToken<
(params?: FindPetsByStatusParams | (() => FindPetsByStatusParams | undefined))
=> ReturnType<typeof httpResource<FindPetsByStatusResponse>>
>('FIND_PETS_BY_STATUS');
export function provideFindPetsByStatus(): FactoryProvider {
return {
provide: FIND_PETS_BY_STATUS,
useFactory: () => {
const base = inject(PETSTORE_BASE_URL);
return (params?) =>
httpResource<FindPetsByStatusResponse>(() => {
const _params = typeof params === 'function' ? params() : params;
if (typeof params === 'function' && _params === undefined) return undefined;
return {
url: `${base}/pet/findByStatus`,
params: _params as unknown as Record<string, string | number | boolean | readonly (string | number | boolean)[]>,
};
});
},
};
}Key properties of every generated file:
- Zero runtime overhead — all types come from
schema.d.tsgenerated byopenapi-typescript - Tree-shakeable — a token not injected anywhere is never imported, so esbuild drops the entire file
- Signal-native —
httpResourcere-fires the request automatically when any signal inside the reactive lambda changes - Request suppression — returning
undefinedfrom the lambda keeps the resource idle (no request) - Scoped base URL — each lib has its own
InjectionToken<string>so different parts of an app can point at different environments - Header params —
in: headerparameters become named string args on the factory function and are merged into theheadersobject alongside any auth scheme headers - Cookie params —
in: cookieparameters become named string args (after header params) and are combined into a singleCookieheader value; optional cookies are conditionally included - Binary body — non-json/form/multipart request bodies (e.g.
application/octet-stream,image/*) emitBlob | ArrayBufferas the body type - Response type unions — when an endpoint returns multiple 2xx JSON response codes (e.g. 200 and 201), the generated
Responsetype alias is a union of all of them @deprecatedJSDoc — operations markeddeprecated: truein the spec emit/** @deprecated */above the token constant, surfacing the warning at everyinject()call site- Security tokens — signal-based schemes (
bearer,basic,apiKey) emitInjectionToken<Signal<string | null>>;digestschemes emitInjectionToken<HttpInterceptorFn>+ a named, host-scoped interceptor that delegates only to requests matching the lib's base URL, preventing cross-API conflicts
import { provideHttpClient } from '@angular/common/http';
import { PETSTORE_BASE_URL, provideFindPetsByStatus } from '@angular-openapi-gen/petstore-data-access';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
{ provide: PETSTORE_BASE_URL, useValue: 'https://petstore3.swagger.io/api/v3' },
provideFindPetsByStatus(),
],
};@Component({ ... })
export class PetsPageComponent {
private findPetsByStatus = inject(FIND_PETS_BY_STATUS);
readonly status = signal<'available' | 'pending' | 'sold'>('available');
// Thunk → httpResource re-fetches whenever status() changes
readonly pets = this.findPetsByStatus(() => ({ status: this.status() }));
}@if (pets.isLoading()) { <mat-progress-bar mode="indeterminate" /> }
@for (pet of pets.value() ?? []; track pet.id) {
<p>{{ pet.name }}</p>
}Pass a thunk that returns undefined to suppress the request until conditions are met:
// No request fires until both apiKey and query are set
readonly results = this.youtubeSearch(() =>
this.apiKey() && this.query()
? { q: this.query(), key: this.apiKey()! }
: undefined
);# Serve the demo app
npx nx serve api-explorer
# Production build
npx nx build api-explorer
npx nx build api-explorer --stats-json # include esbuild bundle stats
# Run tests
npx nx test openapi-resource-gen # generator unit tests
npx nx e2e api-explorer-e2e # Playwright E2E tests
# Lint everything
npx nx run-many -t lint
# Type-check everything
npx nx run-many -t typecheck
# Generate from a local file
npx nx g @constantant/openapi-resource-gen:api-resource \
--specPath=specs/myapi.yaml \
--outputDir=libs/myapi-data-access/src \
--baseUrlToken=MYAPI_BASE_URL
# Generate from a URL (no curl step needed)
npx nx g @constantant/openapi-resource-gen:api-resource \
--specPath=https://petstore3.swagger.io/api/v3/openapi.yaml \
--outputDir=libs/petstore-data-access/src \
--baseUrlToken=PETSTORE_BASE_URL
# Or declare a generate target in project.json and run:
npx nx run petstore-data-access:generatePublished on npm: @constantant/openapi-resource-mocks
A companion package that provides zero-HTTP, pure-DI mocks for generated tokens. Key features:
provideMockResourceBus()— registers the bus; exposeswindow.__openApiMocks__andopenApiMock(key)for PlaywrightprovideMockResource(token, key, initialBehavior?, meta?)— replaces a token's factory with a mock; the optionalmeta(MockResourceMeta) is embedded automatically in generated.mock.tsfiles and used by the DevTools panel to resolve response schemas- DOM event bridge (
openapi-mock-event/openapi-mock-control) — lets the Chrome Extension DevTools panel observe and control mocks in real time injectMockResource<T>(key)— retrieves theMockResourceRef<T>for a registered key inside an injection context (e.g.TestBed.runInInjectionContext)MockResourceRef<T>—resolve(),setLoading(),fail(),reset(),simulateProgress(),getHistory()/testingsub-entry —mockResource(token, behavior?)returns aMockResourceHandle<T>extendingFactoryProvider; drop directly intoTestBedproviders without a full bus setup; supports{ sequence: [...] }for multi-step per-call responses
See tools/openapi-resource-mocks/README.md for full documentation.
Current version: 0.7.0 | Status: pending Chrome Web Store review
A Chrome DevTools panel that connects to any Angular app running @constantant/openapi-resource-mocks. It lists every registered mock token, shows live state, and lets you resolve, fail, catch, or reset mocks without touching code.
Key panel features:
- Mock table — live status, catch mode toggle, resolve/fail/reset actions per token
- Respond tab — JSON editor with schema-aware ⚡ Example generation and ✓ Validate; delay control; catch mode release
- History tab — reverse-chronological event log; for
request/caughtevents shows the filled URL (GET /pet/42), path-param rows labeled by name, and Query / Body sections for remaining args; binary payloads ([FormData],[Blob], etc.) shown as inline badges - Specs tab — import
mocks.manifest.jsonor a full OpenAPI spec (JSON or YAML, file or URL) to enable schema-aware features - Scenarios toolbar button — save named snapshots of the full mock table state, load / delete them, or export / import as JSON for cross-machine sharing
- + New mock button — create a panel-managed (local) mock before
provideMockResource()exists in the app, pre-configure catch mode and a response value, and have it promoted in-place when the key is registered
Chrome Web Store: pending review — use Load unpacked for now.
- Clone the repo and install dependencies:
npm ci - Build the extension:
npx nx run openapi-resource-devtools:build - Open
chrome://extensions→ enable Developer mode → Load unpacked → selectdist/tools/openapi-resource-devtools/
See tools/openapi-resource-devtools/README.md for full documentation.
The generator is released via the Release GitHub Actions workflow (.github/workflows/release.yml), triggered manually from the Actions tab.
What the workflow does:
- Runs
nx release --skip-publish— determines the version bump from conventional commits (fix:→ patch,feat:→ minor), updatespackage.json, writesCHANGELOG.md, creates a git commit and tag - Pushes the version commit and tag to
master - Creates a GitHub Release with changelog notes extracted from
CHANGELOG.md - Builds the package with
nx build openapi-resource-gen --skip-nx-cache - Publishes
@constantant/openapi-resource-genand@constantant/openapi-resource-mocksto npm with provenance, authenticating through npm trusted publishing (OIDC): each package has a trusted publisher configured on npmjs.com (constantant/angular-openapi-gen, workflowrelease.yml), so there is no npm token to rotate
The workflow is idempotent — if the current version is already on npm it skips publishing gracefully.
Inputs: specifier / preid force the bump, projects limits the release to some packages (e.g. openapi-resource-gen), dryRun stops before pushing or publishing, and skipVersioning re-runs only the build and publish after a failed publish. A forced specifier bumps every package unless projects is set, including ones with no changes.
Note on nx release commit detection: nx release counts only commits that touch files within tools/openapi-resource-gen/. Workflow-only changes (e.g. editing .github/) do not trigger a version bump.
Note on branch protection: master is protected (PRs require CI + a code-owner review, linear history). The release workflow checks out with GH_PAT (a repo admin PAT stored in GitHub secrets) instead of GITHUB_TOKEN — GITHUB_TOKEN cannot bypass branch protection's required status checks even with enforce_admins: off, so the version-bump commit and tag push would fail without a PAT.
The Chrome Extension is released via a separate Release Extension workflow (.github/workflows/release-extension.yml). It bumps manifest.json, builds and zips the extension, creates a GitHub Release, and uploads to the Chrome Web Store. Required secrets: GH_PAT, CHROME_EXTENSION_ID, CHROME_PUBLISHER_ID, CHROME_CLIENT_ID, CHROME_CLIENT_SECRET, CHROME_REFRESH_TOKEN.
Contributions are welcome! Please read CONTRIBUTING.md before
opening a PR — in particular the rule that generated code under libs/*/src/ is
never hand-edited (fix the generator and regenerate instead).
- Code of Conduct: CODE_OF_CONDUCT.md
- Security policy: SECURITY.md
- Questions & ideas: GitHub Discussions
Every range below is exercised in CI (.github/workflows/compat.yml,
run locally with npm run compat -- angular 21 or npm run compat -- nx 22). Each job builds a
throwaway workspace with that exact version and runs the code under test, including wire-level
tests against a real HttpClient.
| What | Supported | Verified against |
|---|---|---|
| Generated data-access libs (the code the generator emits) | Angular 20+ | 20.0.0, 20.x, 21.x, 22.x |
@constantant/openapi-resource-mocks |
Angular 20+ | 20.0.0, 20.x, 21.x, 22.x |
@constantant/openapi-resource-gen (the Nx generator) |
Nx 20+ | 20.x, 21.x, 22.x, 23.x |
Notes:
- Angular 20 is the floor because the mock
ResourcemirrorsResourceStatusas string values ('idle','resolved', …), which Angular 19 declares as a numeric enum. The generated libs alone also compile and pass the wire tests on 19.2, but that isn't a supported or CI-tested range. - Upload progress (
--reportProgress) needs an XHR-backedHttpClient. On Angular 20 and 21 that is the default; on Angular 22+ the default isfetch, which cannot report upload progress, so useprovideHttpClient(withXhr()).
httpResource()— signal-native HTTP wrapper; re-fires when signals inside its reactive lambda change; returnsundefinedfrom the lambda to suppress the requestInjectionTokenwith factory — tree-shakeable whenprovidedIn: 'root'; or use the emittedprovideX()helper for scope control- Standalone components — no
NgModule, nozone.js @if/@for/@switch— Angular 17+ control flow syntax throughoutOnPush— default change detection; do not set it explicitly- Signals —
signal()+computed()for all local state; no RxJS in components