https://github.com/constantant/angular-openapi-gen
https://github.com/constantant/angular-openapi-gen
Last synced: about 1 month ago
JSON representation
- Host: GitHub
- URL: https://github.com/constantant/angular-openapi-gen
- Owner: constantant
- License: mit
- Created: 2026-06-05T22:35:46.000Z (about 2 months ago)
- Default Branch: master
- Last Pushed: 2026-06-08T00:36:56.000Z (about 2 months ago)
- Last Synced: 2026-06-08T02:24:10.518Z (about 2 months ago)
- Language: TypeScript
- Size: 5.03 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
Awesome Lists containing this project
- fucking-awesome-angular - angular-openapi-gen - Tree-shakeable, signal-native API clients generator. (Development Utilities / Generators and Scaffolding)
- awesome-angular - angular-openapi-gen - Tree-shakeable, signal-native API clients generator. (Development Utilities / Generators and Scaffolding)
README
# angular-openapi-gen
[](https://github.com/constantant/angular-openapi-gen/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@constantant/openapi-resource-gen)
[](https://www.npmjs.com/package/@constantant/openapi-resource-mocks)
[](LICENSE)
[](https://angular.dev)
> **Requires Angular 22+ and Nx 22+.**
> The generated code uses [`httpResource()`](https://angular.dev/guide/http/http-resource),
> 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.
---
## 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`](https://www.npmjs.com/package/@constantant/openapi-resource-gen)**
### Quick start
**Step 1 — install the generator** (once per workspace):
```bash
npm install -D @constantant/openapi-resource-gen
```
**Step 2 — generate a data-access lib** from any OpenAPI 3.x spec (local file or URL):
```bash
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:**
```typescript
// 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(),
],
};
```
```typescript
// 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 | — | 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` |
| `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`](tools/openapi-resource-gen/README.md) for full documentation.
---
## 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`:
```typescript
// 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>
>('FIND_PETS_BY_STATUS');
export function provideFindPetsByStatus(): FactoryProvider {
return {
provide: FIND_PETS_BY_STATUS,
useFactory: () => {
const base = inject(PETSTORE_BASE_URL);
return (params?) =>
httpResource(() => {
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,
};
});
},
};
}
```
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` 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>`; `digest` schemes emit `InjectionToken` + 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`
```typescript
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
```typescript
@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() }));
}
```
```html
@if (pets.isLoading()) { }
@for (pet of pets.value() ?? []; track pet.id) {
{{ pet.name }}
}
```
### Conditional requests (thunk returning undefined)
Pass a thunk that returns `undefined` to suppress the request until conditions are met:
```typescript
// 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
```bash
# 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`](https://www.npmjs.com/package/@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
- `MockResourceRef` — `resolve()`, `setLoading()`, `fail()`, `reset()`, `simulateProgress()`, `getHistory()`
See [`tools/openapi-resource-mocks/README.md`](tools/openapi-resource-mocks/README.md) for full documentation.
---
## Chrome DevTools Extension — OpenAPI Resource Mocks DevTools
Current version: **0.6.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. The **Specs** tab lets you import a `mocks.manifest.json` or full OpenAPI spec to enable schema-aware features in the Respond tab: auto-generated example payloads (⚡ Example) and JSON schema validation (✓ Validate).
### 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`](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 to npm as `@constantant/openapi-resource-gen` using `NPM_TOKEN` stored 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`.
---
## Contributing
Contributions are welcome! Please read **[CONTRIBUTING.md](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](CODE_OF_CONDUCT.md)
- Security policy: [SECURITY.md](SECURITY.md)
- Questions & ideas: [GitHub Discussions](https://github.com/constantant/angular-openapi-gen/discussions)
---
## 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