Requires Angular 22+ and Nx 22+. The generated code uses
httpResource(), which is only available from Angular 22 onwards.
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.
| 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 | — | Local path or https:// URL to the OpenAPI 3.x YAML or JSON spec |
outputDir |
yes | — | Output directory relative to workspace root |
baseUrlToken |
no | API_BASE_URL |
Name of the base-URL injection token |
tagFilter |
no | all tags | Comma-separated list of tags to include |
namingConvention |
no | kebab |
kebab or camel — controls file names |
providedIn |
no | none |
none (use provideX() helpers) or root (self-registering) |
includeMocks |
no | false |
Co-generate .mock.ts providers, index.mock.ts barrels, and mocks.manifest.json — requires @constantant/openapi-resource-mocks |
includeMswHandlers |
no | false |
Co-generate .msw.ts MSW 2.x handler files and index.msw.ts barrels — requires msw >= 2.0.0 |
specId |
no | derived | Identifier embedded in MockResourceMeta and mocks.manifest.json. Defaults to baseUrlToken with _BASE_URL stripped (e.g. PETSTORE_BASE_URL → petstore). Must match when importing into the DevTools panel. |
verbose |
no | false |
Print a +/~/- summary of created, updated, and deleted files after generation. |
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 to npm as
@constantant/openapi-resource-genusingNPM_TOKENstored in GitHub secrets
The workflow is idempotent — if the current version is already on npm it skips publishing gracefully.
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
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