Skip to content

Repository files navigation

angular-openapi-gen

CI npm: openapi-resource-gen npm: openapi-resource-mocks License: MIT Angular 20+

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 InjectionToken whose function returns a signal-native httpResource (or, per endpoint, an Observable via HttpClient). 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 /testing entry point, optional MSW handlers and a Chrome DevTools panel to catch, answer and replay requests.

How it compares

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.


What's in this repo

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

The generator — @constantant/openapi-resource-gen

Published on npm: @constantant/openapi-resource-gen

Quick start

Step 1 — install the generator (once per workspace):

npm install -D @constantant/openapi-resource-gen

Step 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_URL

Step 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.

Generator options

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.


Generated token pattern

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.ts generated by openapi-typescript
  • Tree-shakeable — a token not injected anywhere is never imported, so esbuild drops the entire file
  • Signal-native — httpResource re-fires the request automatically when any signal inside the reactive lambda changes
  • Request suppression — returning undefined from 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: header parameters become named string args on the factory function and are merged into the headers object alongside any auth scheme headers
  • Cookie params — in: cookie parameters become named string args (after header params) and are combined into a single Cookie header value; optional cookies are conditionally included
  • Binary body — non-json/form/multipart request bodies (e.g. application/octet-stream, image/*) emit Blob | ArrayBuffer as the body type
  • Response type unions — when an endpoint returns multiple 2xx JSON response codes (e.g. 200 and 201), the generated Response type alias is a union of all of them
  • @deprecated JSDoc — operations marked deprecated: true in the spec emit /** @deprecated */ above the token constant, surfacing the warning at every inject() call site
  • Security tokens — signal-based schemes (bearer, basic, apiKey) emit InjectionToken<Signal<string | null>>; digest schemes emit InjectionToken<HttpInterceptorFn> + a named, host-scoped interceptor that delegates only to requests matching the lib's base URL, preventing cross-API conflicts

Using generated tokens

1. Register providers in app.config.ts

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(),
  ],
};

2. Inject and call in a component

@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>
}

Conditional requests (thunk returning undefined)

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
);

Common commands

# 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:generate

Mock bus — @constantant/openapi-resource-mocks

Published 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; exposes window.__openApiMocks__ and openApiMock(key) for Playwright
  • provideMockResource(token, key, initialBehavior?, meta?) — replaces a token's factory with a mock; the optional meta (MockResourceMeta) is embedded automatically in generated .mock.ts files 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 the MockResourceRef<T> for a registered key inside an injection context (e.g. TestBed.runInInjectionContext)
  • MockResourceRef<T> — resolve(), setLoading(), fail(), reset(), simulateProgress(), getHistory()
  • /testing sub-entry — mockResource(token, behavior?) returns a MockResourceHandle<T> extending FactoryProvider; drop directly into TestBed providers without a full bus setup; supports { sequence: [...] } for multi-step per-call responses

See tools/openapi-resource-mocks/README.md for full documentation.


Chrome DevTools Extension — OpenAPI Resource Mocks DevTools

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/caught events 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.json or 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

Install

Chrome Web Store: pending review — use Load unpacked for now.

  1. Clone the repo and install dependencies: npm ci
  2. Build the extension: npx nx run openapi-resource-devtools:build
  3. Open chrome://extensions → enable Developer mode → Load unpacked → select dist/tools/openapi-resource-devtools/

See tools/openapi-resource-devtools/README.md for full documentation.


Release workflow

The generator is released via the Release GitHub Actions workflow (.github/workflows/release.yml), triggered manually from the Actions tab.

What the workflow does:

  1. Runs nx release --skip-publish — determines the version bump from conventional commits (fix: → patch, feat: → minor), updates package.json, writes CHANGELOG.md, creates a git commit and tag
  2. Pushes the version commit and tag to master
  3. Creates a GitHub Release with changelog notes extracted from CHANGELOG.md
  4. Builds the package with nx build openapi-resource-gen --skip-nx-cache
  5. Publishes @constantant/openapi-resource-gen and @constantant/openapi-resource-mocks to npm with provenance, authenticating through npm trusted publishing (OIDC): each package has a trusted publisher configured on npmjs.com (constantant/angular-openapi-gen, workflow release.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.


Contributing

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).


Compatibility

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 Resource mirrors ResourceStatus as 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-backed HttpClient. On Angular 20 and 21 that is the default; on Angular 22+ the default is fetch, which cannot report upload progress, so use provideHttpClient(withXhr()).

Angular 22 patterns used

  • httpResource() — signal-native HTTP wrapper; re-fires when signals inside its reactive lambda change; returns undefined from the lambda to suppress the request
  • InjectionToken with factory — tree-shakeable when providedIn: 'root'; or use the emitted provideX() helper for scope control
  • Standalone components — no NgModule, no zone.js
  • @if / @for / @switch — Angular 17+ control flow syntax throughout
  • OnPush — default change detection; do not set it explicitly
  • Signals — signal() + computed() for all local state; no RxJS in components

About

Tree-shakeable, signal-native API clients generator

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages