This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Angular 22 · Nx monorepo · InjectionToken-based REST data access from OpenAPI specs via httpResource.
Implementation status: Three published packages —
@constantant/openapi-resource-gen(v1.8.0),@constantant/openapi-resource-mocks(v0.5.0), Chrome Extensionopenapi-resource-mocks-devtools(v0.7.0). Data-access libs:github,petstore,weather,youtube— all generated with--includeMocks=true --specId=<name>.apps/api-exploreris wired up with routes and Angular Material UI.apps/devtools-panelis the Angular 22 panel app bundled inside the Chrome Extension.
Build an Nx generator (openapi-resource-gen) that reads an OpenAPI 3.x spec and emits one
InjectionToken per endpoint, each in its own .ts file, enabling maximum tree-shaking.
Validate the approach with a demo Angular 22 app (api-explorer) that consumes multiple
real-world APIs but ships only the endpoints it actually injects.
apps/
api-explorer/ # Angular 22 standalone app, zoneless, OnPush default
devtools-panel/ # Angular 22 panel app — bundled inside the Chrome Extension
devtools-panel-e2e/ # Playwright E2E tests for devtools-panel (port 4202)
libs/
github-data-access/ # generated — github/rest-api-description
petstore-data-access/ # generated — OAI petstore spec
weather-data-access/ # generated — Open-Meteo forecast API
youtube-data-access/ # generated — YouTube Data API v3 (76 endpoints)
tools/
openapi-resource-gen/ # Nx plugin (generator + executor), published as @constantant/openapi-resource-gen
src/
generators/
api-resource/
schema.json
generator.ts
parse-spec.ts
render-token.ts
endpoint-model.ts
files/ # EJS templates
__tag__/
__operationId__.token.ts__tmpl__
api-base-url.token.ts__tmpl__
index.ts__tmpl__
executors/
generate/
schema.json
executor.ts # wraps the generator for nx run project:generate
openapi-resource-mocks/ # published as @constantant/openapi-resource-mocks
# mock bus: window API, DOM events, Chrome Extension bridge
# src/testing.ts — /testing sub-entry (mockResource, MockResourceHandle)
openapi-resource-devtools/ # Chrome Extension shell (manifest, content script, service worker,
# devtools page). Panel UI lives in apps/devtools-panel/.
src/
background/sw.ts # service worker — routes messages between content scripts and panel
content/content.ts # content script — bridges window DOM events ↔ background SW
devtools/devtools.ts # creates the DevTools panel page
manifest.json # version source of truth for the extension
CHANGELOG.md
scripts/release.mjs # standalone release script (bumps manifest, writes changelog, tags)
httpResource()— stable (Angular 22). Reactive wrapper aroundHttpClient. ReturnsHttpResourceRef<T>with.value(),.isLoading(),.error()signals. The lambda re-runs whenever signals inside it change (likeswitchMapbut declarative). Returnsundefinedfrom the lambda to suppress the request entirely (resource stays idle).@Service()decorator — stable (Angular 22). Replaces@Injectable({ providedIn: 'root' }). Default behaviour is root-scoped singleton, tree-shakeable.OnPush— now the defaultChangeDetectionStrategy. New components get it automatically. UseChangeDetectionStrategy.Eageronly for legacy interop.InjectionTokenwith factory — tree-shakeable whenprovidedIn: 'root'and factory usesinject()internally. This is the core pattern of every generated token file.- Signal Forms (
form()+FormFielddirective) — stable (Angular 22). Use for mutation pages. - Zoneless — default for new projects. No
zone.jsimport needed.
// 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)[]>,
};
});
},
};
}Rules:
- Query params (any method) use a block-body lambda with
_paramspre-computation and earlyreturn undefinedguard — this makeshttpResourceidle when the thunk returnsundefined. Shorthand() => ({...})would always fire because the lambda always returns an object. inject()inside factory only — no constructor DI.- Types always sourced from
paths[...]['get']['responses']['200'][...]— never hand-written. - Mutations (POST/PUT/PATCH/DELETE): factory returns
(body: Signal<T> | T) => httpResource(...), addmethod: 'POST'(etc.) andbodyto the resource config. ASignalbody is unwrapped inside the reactive lambda (_body), so it is sent as its value and re-fires when it changes. - Query params on mutations: a
paramsargument after the body ((id, body, params?)). It is required when the spec has a required query param (YouTube'spart); GET keeps its optionalparams?. Required args always precede optional ones. - Path params (e.g.
/pets/{id}): become required args on the returned function, interpolated into the URL string inside the reactive lambda. - Header params (
in: header, e.g.X-Api-Version): become named string args after path params. Required → plainarg: string; optional →arg?: string. Rendered into aheadersobject in the resource config; optional ones use a conditional spread (...(arg != null ? {...} : {})). Auth scheme headers appear alongside them in the sameheadersblock.
When the spec has security schemes, the generator emits one additional file per scheme. Two different patterns are used depending on the scheme kind.
Signal-based (bearer, oauth2, openIdConnect, basic, apiKey-header, apiKey-query) —
emits InjectionToken<Signal<string | null>>. Endpoint tokens inject these optionally and
merge auth into the request headers/params:
// libs/youtube-data-access/src/oauth2.security-token.ts
import { InjectionToken, Signal } from '@angular/core';
export const OAUTH2 = new InjectionToken<Signal<string | null>>('OAUTH2');const oauth2 = inject(OAUTH2, { optional: true }); // Signal<string | null> | null
// In the reactive lambda:
headers: {
...(oauth2?.() != null ? { Authorization: `Bearer ${oauth2()}` } : {}),
},Interceptor-based (digest) — Digest is a challenge-response protocol (URL + method +
nonce hash) that cannot be computed as a static header. The generator emits
InjectionToken<HttpInterceptorFn> plus a named, host-scoped interceptor wrapper:
// libs/myapi-data-access/src/digest-auth.security-token.ts
import { InjectionToken, inject } from '@angular/core';
import { HttpInterceptorFn } from '@angular/common/http';
import { MYAPI_BASE_URL } from './api-base-url.token';
export const DIGEST_AUTH = new InjectionToken<HttpInterceptorFn>('DIGEST_AUTH');
export const myapiDigestAuthInterceptor: HttpInterceptorFn = (req, next) => {
const base = inject(MYAPI_BASE_URL);
if (!req.url.startsWith(base)) return next(req); // scoped to this API only
const fn = inject(DIGEST_AUTH, { optional: true });
if (!fn) return next(req);
return fn(req, next); // req.urlWithParams, req.method, req.body available
};The interceptor name is derived from the base URL token (MYAPI_BASE_URL → myapi) and the
scheme name, making it unique per API. Multiple APIs with digest auth have distinct interceptors
and distinct base URL checks — no cross-API conflicts. The consumer's implementation receives
the full HttpRequest, which carries everything needed for the RFC 7616 hash.
Consumer wires it in app.config.ts:
provideHttpClient(withInterceptors([myapiDigestAuthInterceptor])),
{ provide: DIGEST_AUTH, useValue: myDigestInterceptorFn },- If
specPathis anhttp:///https://URL, download to a temp file first (Nodehttps/http). js-yaml+stripNonSchemaRefs()— load YAML, strip non-spec$reflinks (markdown, images).- Swagger 2.0 (
swagger: '2.x') is converted to OpenAPI 3.0 in memory first (swagger2.ts: globalproduces/consumespushed into operations, then@scalar/openapi-upgrader; opt out withconvertSwagger2: false). Then validate: must be OpenAPI 3.x (openapifield starts with'3') and have apathskey. Throws a descriptive error otherwise. openapi-typescript(programmatic API) — emitschema.d.tsfrom the cleaned spec.@apidevtools/swagger-parser— dereference all$refchains for endpoint extraction.parseSecuritySchemes(api)— extract security schemes intoSecuritySchemeModel[].buildEndpoints(api, tags, convention)— map each operation toEndpointModel.renderTokenFile()/renderSecurityTokenFile()/renderMockFile()/renderMswFile()— emit token, mock, and MSW handler files as strings.- Stale file cleanup — snapshot
.token.ts,.security-token.ts,.mock.ts,.msw.ts,mocks.manifest.json,index.ts,index.mock.ts, andindex.msw.tsfiles before the run; delete any that weren't produced this run. @nx/devkitformatFiles()— run Prettier over all written files.
{
"specPath": "local path OR https:// URL to the OpenAPI YAML/JSON file",
"outputDir": "output directory inside libs/",
"baseUrlToken": "optional: name of the base URL token (default: API_BASE_URL)",
"tagFilter": "optional: only generate tokens for these tags",
"namingConvention": "camel | kebab (default: kebab for filenames, SCREAMING_SNAKE for token names)",
"providedIn": "none | root (default: none)",
"includeMocks": "true | false (default: false) — emit *.mock.ts alongside each *.token.ts",
"includeMswHandlers": "true | false (default: false) — emit *.msw.ts MSW 2.x handler files alongside each *.token.ts; adds /msw path alias to tsconfig.base.json",
"clientType": "httpResource | httpClient (default: httpResource) — httpClient tokens return Observable<T> via HttpClient.request()",
"callOptions": "true | false (default: false) — trailing per-call options argument (HttpContext, headers, withCredentials, httpResource defaultValue/equal/injector/debugName); emits request-options.ts",
"convertSwagger2": "true | false (default: true) — convert Swagger 2.0 specs to OpenAPI 3.0 in memory (false rejects them)",
"readWriteMarkers": "true | false (default: false) — honor readOnly/writeOnly: request bodies (Writable<>) drop readOnly properties, responses/errors (Readable<>) drop writeOnly ones, via openapi-typescript readWriteMarkers",
"reportProgress": "true | false (default: false) — httpClient binary/multipart uploads + blob downloads yield Observable<HttpEvent<T>> (progress + response); httpResource blob downloads get reportProgress: true",
"httpClientTags": "optional: comma-separated tags that use HttpClient regardless of clientType (errors if unmatched)",
"httpClientOperations": "optional: comma-separated operationIds that use HttpClient regardless of clientType (errors if unmatched)",
"validateResponses": "true | false (default: false) — validate JSON responses at runtime against the spec schema via httpResource's parse hook; requires @cfworker/json-schema",
"specId": "string — embedded in MockResourceMeta._meta; must match the specId used when importing the spec into the DevTools panel",
"verbose": "true | false (default: false) — print +/~/- summary of created/updated/deleted files after generation"
}- Each OpenAPI tag becomes one subfolder under
outputDir. - Untagged operations go into
default/. - Each folder gets its own
index.tsbarrel. - Root
index.tsre-exports all folder barrels + security token files.
| Path | Component | Tokens injected | Source lib |
|---|---|---|---|
/ |
DashboardComponent |
GET_USER, LIST_REPOS, FIND_PETS_BY_STATUS, GET_V1_FORECAST |
multiple |
/repos |
ReposPageComponent |
GET_USER, LIST_REPOS |
github-data-access |
/pets |
PetsPageComponent |
FIND_PETS_BY_STATUS |
petstore-data-access |
/weather |
WeatherPageComponent |
GET_V1_FORECAST |
weather-data-access |
/youtube |
YoutubePageComponent |
YOUTUBE_SEARCH_LIST |
youtube-data-access |
MatToolbar— top app barMatSidenav/MatNavList— navigation railMatCard— content panelsMatChipListbox/MatChip— status filter chipsMatTable+MatPaginator— data listsMatProgressBar— loading indicatorMatFormField/MatInput— search and auth key input
| Lib | Spec source | Endpoints |
|---|---|---|
github-data-access |
github/rest-api-description — api.github.com.yaml |
~38 used |
petstore-data-access |
OAI Petstore v3 | 12 |
weather-data-access |
Open-Meteo forecast API | ~5 used |
youtube-data-access |
YouTube Data API v3 | 76 (all) |
Fetch commands:
# GitHub
curl -L https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.yaml \
-o specs/github.yaml
# Petstore
curl -L https://petstore3.swagger.io/api/v3/openapi.yaml \
-o specs/petstore.yaml
# YouTube Data API v3
curl -L https://raw.githubusercontent.com/APIs-guru/openapi-directory/main/APIs/googleapis.com/youtube/v3/openapi.yaml \
-o specs/youtube.yaml# Development
npx nx serve api-explorer # Dev server on http://localhost:4200
npx nx serve devtools-panel # Panel dev server on http://localhost:4200 (or specify port)
# Build
npx nx build api-explorer # Production build
npx nx build api-explorer --stats-json # Include esbuild bundle stats
npx nx run openapi-resource-devtools:build # Build Chrome Extension → dist/tools/openapi-resource-devtools/
# Test
npx nx test api-explorer # Run unit tests (Vitest)
npx nx test devtools-panel # Run panel unit tests (Vitest)
npx nx e2e api-explorer-e2e # Run Playwright E2E tests (port 4200)
npx nx e2e devtools-panel-e2e # Run Playwright E2E tests (port 4202)
npx nx test openapi-resource-gen # Run generator unit tests
npm run compat -- angular 21 # Compat matrix: libs + mocks on that Angular (20-min|20|21|22)
npm run compat -- nx 22 # Compat matrix: generator on that Nx (20|21|22|23)
# Lint / format
npx nx run-many -t lint # Lint all projects
npx prettier --write apps/ # Format code
# Type-check all
npx nx run-many -t typecheck
# Generate a data-access lib from a spec (with mocks for DevTools panel)
npx nx g @constantant/openapi-resource-gen:api-resource \
--specPath=specs/youtube.yaml \
--outputDir=libs/youtube-data-access/src \
--baseUrlToken=YOUTUBE_BASE_URL \
--includeMocks=true \
--specId=youtube
# Inspect bundle after build with --stats-json
npx webpack-bundle-analyzer dist/apps/api-explorer/browser/stats.jsonCRITICAL — always use Nx generators to scaffold new apps, libs, and components. Never hand-craft
project.json,tsconfig.json, orangular.jsonfiles from scratch. Use the appropriate generator and then make minimal edits to the generated output only where strictly necessary. This keeps the workspace consistent and ensures all Nx cache inputs, lint rules, and build targets are wired up correctly.
Key generators for this workspace:
# New Angular app
npx nx g @nx/angular:application <name> --directory=apps/<name> --style=less --routing=false --standalone --no-interactive
# New Angular library
npx nx g @nx/angular:library <name> --directory=libs/<name> --standalone --no-interactive
# New standalone component inside an existing project
npx nx g @nx/angular:component <name> --project=<project> --standalone --no-interactive
# New service inside an existing project
npx nx g @nx/angular:service <name> --project=<project> --no-interactiveCRITICAL — generated code is read-only. Files under
libs/*/src/are 100% machine-generated and must never be edited by hand. Any bug or missing feature in a generated file must be fixed in the generator (tools/openapi-resource-gen/) and then the affected lib must be regenerated withnpx nx g @constantant/openapi-resource-gen:api-resource .... Thenode_modules/@constantant/openapi-resource-genpackage is a Windows Junction that points directly attools/openapi-resource-gen, so generator changes are live immediately — no publish step needed.
- All new components: standalone, no
NgModule, nozone.js. - Change detection:
OnPushis the default — do NOT set it explicitly unless overriding. - Signals: prefer
signal()+computed()over RxJS for local state. httpResourcefor all HTTP reads. For mutations, usehttpResourcewithmethodset.- No
@Injectableservices — ever. All shared state and behaviour must be expressed asInjectionTokenwith afactory(usinginject()internally). This applies to everything that would classically be a service: state containers, bridge objects, utilities, etc. UseprovidedIn: 'root'on the token factory for singletons, or return aFactoryProviderfunction (e.g.provideXxx()) when the caller must opt in explicitly. - Template syntax: use
@if,@for,@switch(Angular 17+ control flow). No*ngIf. - Imports: always import from the barrel
index.tsof a lib, never from internal paths. - Do not add
console.logto committed code. - Commit messages:
feat:,fix:,chore:,docs:prefixes.
page (Angular app with @constantant/openapi-resource-mocks)
│ window.__openApiMocks__ (MockResourceBus)
│ window.__oarmPendingCatch__ (pre-injected by SW at page-load start)
│ DOM events: openapi-mock-event / openapi-mock-control
▼
content script (tools/openapi-resource-devtools/src/content/content.ts)
│ chrome.runtime.sendMessage / chrome.runtime.onMessage
▼
background service worker (tools/openapi-resource-devtools/src/background/sw.ts)
│ chrome.runtime.Port (named "devtools-<tabId>")
│ tabCatchModes: Map<tabId, Record<key, boolean>> (in-memory + chrome.storage.session)
▼
devtools panel (apps/devtools-panel/) — Angular 22 app
MOCK_BRIDGE InjectionToken ←→ port.postMessage / port.onMessage
The panel app (apps/devtools-panel/) is a standalone Angular 22 app that runs inside
the Chrome DevTools panel page. It communicates with the inspected page via the
background service worker using named ports.
When a developer enables catch mode on any mock (including a local/unregistered one), the
SW records it in an in-memory tabCatchModes map (mirrored to chrome.storage.session
under oarm_catch_<tabId> for SW sleep survival). On chrome.tabs.onUpdated with
status: 'loading', the SW immediately calls chrome.scripting.executeScript with
injectImmediately: true to set window.__oarmPendingCatch__ = { KEY: true, ... } in the
MAIN world — before Angular's <script type="module"> tags execute.
MockResourceBus constructor reads window.__oarmPendingCatch__ synchronously and
pre-populates catchModeKeys. This ensures the very first _notifyRequest() call (which
fires synchronously inside provideMockResource's factory during Angular bootstrap) is
already intercepted, with no async round-trip required.
apps/devtools-panel/src/app/mock-bridge.token.ts — the core DI bridge. Its factory:
- When running inside Chrome DevTools (
chrome.devtoolsis defined): creates a port, connects to the background SW, and returns a liveMockBridgethat drives the panel. - When running outside Chrome DevTools (dev server, E2E, Vitest): returns a no-op stub
bridge. Guard:
typeof chrome === 'undefined' || !chrome.devtools.
Never provide MOCK_BRIDGE via @Injectable — it must always be the token factory.
MockBridge interface includes two methods for panel-managed (local) mocks:
createLocalMock(key, meta)— adds an entry withstatus: 'local', persists tochrome.storage.local['oarm_local_mocks'](schema:Record<string, { meta, catchMode }>), and auto-selects the new entry in the table.deleteLocalMock(key)— removes the entry and clears it from storage. Guard: only deletes entries whosestate.status === 'local', never live entries.
Local mocks are restored from storage on panel init (after connect()). When mock-keys
arrives containing a key that was local, the entry is promoted in-place: status
transitions to 'idle', catchMode is preserved, and the catch-mode re-send loop fires
the setCatchMode control message to the newly-live bus. The entry is removed from
oarm_local_mocks storage at that point.
- CodeMirror 6 —
apps/devtools-panel/src/app/components/json-editor/— syntax-highlighted JSON editor in the Respond tab. Uses VS Code Dark+ colours. Bidirectional sync with Angular signals via askipSyncflag. - json-schema-faker —
⚡ Generatebutton in Respond tab generates example JSON from the response schema. - @cfworker/json-schema —
✓ Validatevalidates the editor content against the response schema. Critical: the standalonevalidate()export is broken (always returns{valid: true}). Always use:const { Validator } = await import('@cfworker/json-schema'); const result = new Validator(schema).validate(instance);
- js-yaml —
load as yamlLoad— Specs tab accepts.yaml/.ymlfiles and YAML URLs in addition to JSON. - SPEC_STORE token (
apps/devtools-panel/src/app/spec-store.token.ts) — stores OpenAPI specs inchrome.storage.local. Uses a module-levelrewrittenDefsCache: Map<string, …>to defer the expensiverewriteRefs()call on largecomponents/schemasblobs (e.g. github.yaml) until the firstfindSchema()call, avoiding hangs on import. - MockResourceMeta —
{ specId, operationId, path, method, tag? }— embedded in each generated mock file asexport const _meta: MockResourceMeta. Passed toprovideMockResource()so the panel can show operation info in the mock table and Respond tab. - CreateMockDialog —
apps/devtools-panel/src/app/components/create-mock-dialog/— opened by the "+ New mock" button in the mock table. Lets developers create panel-managed (local) mocks beforeprovideMockResource()exists in the app: pick a spec fromSPEC_STORE, select an operation, confirm the auto-generated key (toScreamingSnake(operationId), editable). The key is conflict-checked againstMOCK_BRIDGE.mocks(). UsesMatDialogfrom@angular/material/dialog. toScreamingSnake— exported fromapps/devtools-panel/src/app/spec-store.token.ts. Used byCreateMockDialogto derive the default key from an operationId, matching the generator's naming convention exactly.- ScenarioDialog —
apps/devtools-panel/src/app/components/scenario-dialog/— opened by the "Scenarios" toolbar button. Saves/loads/deletes named mock state snapshots (values + catch modes) and exports/imports current state as JSON. Opened viaMatDialog.open()— must NOT appear in the host component'simports[]. - SCENARIO_STORE token —
apps/devtools-panel/src/app/scenario-store.token.ts—providedIn: 'root'token. Persists namedScenarioobjects tochrome.storage.local['oarm_scenarios'](keyed by name, sorted newest-first).load()callsbridge.sendControl+bridge.setCatchModeper mock entry; skips keys not inbridge.mocks()and skips items with invalid shape.importJson()applies state immediately without saving a named scenario. - History tab request inspector —
apps/devtools-panel/src/app/components/history-tab/— when expanding arequestorcaughtevent, displays: aMETHOD /filled/pathheader (path params substituted from actual arg values usingmeta.path), individual path-param rows labeled by placeholder name, and remaining args labeled Query (GET/HEAD/DELETE) or Body (POST/PUT/PATCH). Binary placeholders ([FormData],[Blob],[ArrayBuffer],[File: name]) are rendered as inline badges rather than JSON strings. The row preview for request events also shows the filled URL. Gracefully falls back to a generic "Request" section whenMockResourceMetais null.
Releases go through .github/workflows/release-extension.yml (triggered manually via
gh workflow run release-extension.yml). Required GitHub secrets:
| Secret | Purpose |
|---|---|
GH_PAT |
Admin PAT — bypasses branch protection to push version bump commit + tag |
CHROME_EXTENSION_ID |
CWS extension ID |
CHROME_PUBLISHER_ID |
Publisher account ID (from CWS developer console URL) |
CHROME_CLIENT_ID |
OAuth2 client ID |
CHROME_CLIENT_SECRET |
OAuth2 client secret |
CHROME_REFRESH_TOKEN |
OAuth2 refresh token |
The release script (tools/openapi-resource-devtools/scripts/release.mjs) bumps
manifest.json, writes CHANGELOG.md, creates an annotated git tag
(git tag -a openapi-resource-devtools@<version>). The workflow then pushes the tag
explicitly (git push origin "refs/tags/<tag>") — git push --follow-tags skips
lightweight tags, so explicit push is required.
This is a public, MIT-licensed repo open to outside contributions. Community
health files live at the root and under .github/:
LICENSE,CONTRIBUTING.md,CODE_OF_CONDUCT.md,SECURITY.md.github/CODEOWNERS(* @constantant),.github/dependabot.yml.github/pull_request_template.md,.github/ISSUE_TEMPLATE/*(YAML forms)
master is branch-protected — no direct pushes; all changes go via PR:
- the CI
mainjob must pass, - 1 approving code-owner review is required,
- linear history (squash/rebase merges only — no merge commits),
enforce_adminsis off so the release workflow'sGITHUB_TOKENcan still push the version bump commit + tag duringnx release.
When making changes here, branch off master and open a PR; use a
Conventional Commits PR title (it becomes the squash commit). See
CONTRIBUTING.md for the full workflow.
| Decision | Choice | Reason |
|---|---|---|
| Token granularity | One file per endpoint | Enables file-level tree-shaking by esbuild |
| Type source | openapi-typescript paths type |
Zero runtime, fully typed, no codegen bloat |
| HTTP primitive | httpResource (stable, Angular 22) |
Signal-native, auto-cancels stale requests |
| Mutation pattern | Factory returns (body) => httpResource(...) |
Consistent API surface for GET and mutations |
| Base URL injection | Named InjectionToken<string> per lib |
Lets apps override URL per environment |
| Params type | Typed via paths[...]['parameters']['query'] |
Full type safety, no manual interfaces |
| Request suppression | Block-body lambda with early return undefined |
Shorthand () => ({url}) always fires; returning undefined from lambda makes resource idle |
| Security tokens — signal | `InjectionToken<Signal<string | null>>` per scheme |
| Security tokens — digest | InjectionToken<HttpInterceptorFn> + named host-scoped interceptor |
Challenge-response at HTTP layer; base URL token prevents cross-API interceptor conflicts |
| Remote spec URL | specPath accepts http:///https:// URLs — downloads to a temp file, then processes identically to local files |
Eliminates the curl pre-step; temp file is cleaned up in the finally block regardless of success/failure |
| Header params | in: header params become named string args (required or optional), rendered into a headers block alongside auth scheme headers |
Consistent with how path params are surfaced; keeps the public API surface explicit and typed |
| Cookie params | in: cookie params become named string args (after header params) and are combined into a single Cookie header using [...].join('; '); optional cookies use a conditional spread |
Cookie params work in SSR (Node HttpClient); browser Cookie header is a forbidden header — document that constraint at usage time rather than in the type |
@deprecated JSDoc |
Generator emits /** @deprecated */ above the InjectionToken constant when operation.deprecated === true in the spec |
TypeScript deprecation warning at the inject() call site; no runtime cost |
| Response type unions | Generator collects all 2xx codes with application/json content (not just the first); emits a |-union type alias when multiple codes exist |
Some endpoints legitimately return 200 (update) or 201 (create) with different shapes; a union preserves that information instead of silently picking one |
| Binary body | When requestBody has no json/form/multipart content type (e.g. application/octet-stream, image/*), the generated Body type alias is Blob | ArrayBuffer instead of the paths chain |
paths[...]['requestBody']['content']['application/octet-stream'] would be string | Blob from openapi-typescript — not useful for Angular's HttpClient which needs the actual binary object |
| Stale file cleanup | Before generation, snapshot .token.ts, .security-token.ts, .mock.ts, mocks.manifest.json, index.ts, and index.mock.ts files; after generation, delete any that weren't produced this run |
Prevents phantom exports when endpoints are removed or tagFilter narrows the output; including barrel files ensures orphaned tag folders (with their index.ts) are also removed |
| Nx executor | @constantant/openapi-resource-gen:generate executor wraps the generator so users can declare a generate target in project.json |
nx run mylib:generate is easier to remember and can be wired into CI; uses FsTree+flushChanges from nx/src/generators/tree (not in @nx/devkit public API) |
| Lint cache invalidation | @nx/eslint:lint has an externalDependencies input listing the ESLint plugin packages (eslint, angular-eslint, typescript-eslint, @eslint/js, …) in nx.json |
A rule-strengthening dependency bump (e.g. an angular-eslint major) must re-lint against current source, not return a stale cached "pass". Without this, the angular-eslint 22 upgrade merged green while leaving master failing prefer-on-push-component-change-detection |
includeMocks + MockResourceMeta |
Generator emits *.mock.ts alongside each *.token.ts; each mock file exports _meta: MockResourceMeta with specId, operationId, path, method, tag |
DevTools panel needs this metadata to display operation info and match mock keys to spec entries |
| Lazy definitions cache in SPEC_STORE | rewrittenDefsCache: Map<string, Record<string, unknown>> defers rewriteRefs() on components/schemas to first findSchema() call; separate rawDefinitions field in SpecEntry avoids embedding the full definitions blob into every per-operation schema |
github.yaml has ~30MB of definitions; embedding them N times in storage caused hangs. Deferring to first use keeps import fast |
@cfworker/json-schema Validator class |
Use new Validator(schema).validate(instance), NOT the standalone validate() function |
The standalone validate export is broken — always returns {valid: true}. The Validator class API works correctly and resolves $ref/definitions |
| Respond tab pinned footer | schema-section, delay-row, and action-row moved out of the scrollable .respond-body into a flex-shrink: 0 .respond-footer below it |
When the editor grows tall, schema/delay/actions were scrolled off-screen. Pinning them ensures they are always reachable |
| Local (unregistered) mocks | MockBridge.createLocalMock() adds a panel-managed entry with status: 'local'; persisted in chrome.storage.local['oarm_local_mocks'] with catchMode; promoted in-place when the app registers the key |
Developer pre-configures a mock (catch mode, Respond tab value) before writing the Angular code. All controls work identically to live mocks; control messages to the page are silently ignored until the key is registered |
| Catch-mode pre-injection | SW tracks catch modes in memory + chrome.storage.session['oarm_catch_<tabId>']; on tabs.onUpdated(loading) injects window.__oarmPendingCatch__ via executeScript(injectImmediately:true); MockResourceBus constructor reads and pre-populates catchModeKeys |
provideMockResource calls _notifyRequest() synchronously during Angular bootstrap — before any async panel message can arrive. Pre-injection closes this race so the very first request is caught |
includeMswHandlers |
Generator emits *.msw.ts alongside each *.token.ts; each file exports listPetsHandler(body?) + listPetsHandlers array; path params converted {id} → :id; DELETE/no-response endpoints use new HttpResponse(null, { status: 204 }); root index.msw.ts barrel + /msw tsconfig alias added automatically |
MSW 2.x is the standard for component-test mocking; this lets consumers drop handlers directly into server.use(listPetsHandlers) without writing boilerplate |
clientType / HttpClient tokens |
Per-endpoint choice (lib default + httpClientTags / httpClientOperations overrides). httpClient tokens return (args) => Observable<T> using http.request(METHOD, url, opts); params/body are plain values; validation runs via map(). Mock files use provideMockObservable() (cold: each subscribe = one request, settles via _onSettle on the ref; a panel resolve/fail with nothing pending is stored per key and replayed by the next request). Demo: weather and youtube fully, petstore (store tag) and github (users tag) mixed, consumed via rxResource — whose value() throws in error state, so guard with hasValue() |
Some consumers want RxJS/interceptor-friendly cold calls instead of signal resources; one token pattern per endpoint keeps tree-shaking and the mock/DevTools flow intact |
| Per-call options | Opt-in callOptions: a trailing options argument. httpResource tokens get a generic XxxFn so a defaultValue narrows value() to T; httpClient tokens take CallOptions. Types derive from Angular's HttpResourceRequest/HttpResourceOptions (so they match the installed version) and splitCallOptions routes fields by key list, never naming debugName in code. Spec-controlled keys are dropped at runtime. Mocks record _meta.args |
Spec-driven requests can't carry an HttpContext, extra headers or a defaultValue. Version-derived types keep Angular 20.0.0 (no debugName) compiling; the mock honours defaultValue so mock mode matches the real resource. petstore-data-access is generated with it |
| Swagger 2.0 input | Swagger 2.0 input is converted in memory by swagger2.ts (@scalar/openapi-upgrader, loaded via require() with a dynamic-import() fallback because it is ESM-only). Global produces/consumes are first pushed down into operations; convertSwagger2: false rejects Swagger 2.0 instead |
The upgrader applies a global produces over an operation's own, which turned image/png downloads into JSON (generator picked the wrong variant). Verified on the real Petstore v2, a compiled integration spec, and Node 20.18 and 24 |
readWriteMarkers |
Opt-in readWriteMarkers: calls openapi-typescript with { readWriteMarkers: true } (adds $Read/$Write/Readable/Writable to schema.d.ts), then wraps XxxBody in Writable<> and XxxResponse / XxxError / discriminated-variant types in Readable<> (inside Readonly<> with --readonlyResponses). Binary bodies and text/blob responses are left alone |
Without it one type serves both directions, so a request body requires server-generated ids and a response claims write-only fields (passwords). Verified by compiling real generator output with @ts-expect-error probes; no measurable tsc cost on the 5 MB GitHub schema |
| Compatibility floor | Generated libs and openapi-resource-mocks: Angular ≥ 20; openapi-resource-gen: Nx ≥ 20. Enforced by .github/workflows/compat.yml → tools/compat/run.mjs (a throwaway workspace per version: Angular 20.0.0 and 20.x–22.x, Nx 20–23; strict tsc + wire-level tests, or the generator suite + a real end-to-end generation). Peer ranges match the matrix |
The old >=22 was declared, not required: generated code only uses APIs present since Angular 19.2. 20 is the floor because the mock Resource mirrors ResourceStatus as strings (a numeric enum in 19) and 19 is out of support. Upload progress needs XHR: default on 20/21, withXhr() on 22+ |
reportProgress |
Opt-in. httpClient upload/blob-download endpoints yield Observable<HttpEvent<T>> (observe: 'events', reportProgress: true); validation maps only the Response event. Mock files use provideMockHttpEvents() (emits Sent, UploadProgress/DownloadProgress from setProgress/simulateProgress, then Response). httpResource only gets reportProgress: true on blob downloads |
Angular 22's httpResource().progress() handles DownloadProgress only and ignores uploads, so a real upload progress bar needs the HttpClient flavour. Upload progress also needs provideHttpClient(withXhr()) (the fetch backend can't report it); api-explorer uses withXhr(), and its Pets page shows it |
/testing entry point |
@constantant/openapi-resource-mocks/testing exports mockResource(TOKEN, behavior?) returning MockResourceHandle<T>, plus mockObservable / mockHttpEvents for httpClient tokens (cold: each subscription consumes the next behavior; mockHttpEvents also emits Sent + progress events) — a FactoryProvider plus .ref, .calls, .expectCalled(), .expectCalledWith(). Supports { sequence: [...] } for multi-step scenarios. No MockResourceBus, no DOM events — pure signal state |
Vitest/Jasmine component tests need zero-infrastructure mocking; sequence mode enables pagination, retry, and error-then-success patterns in a single test |
| Scenario save/load | SCENARIO_STORE token (providedIn: 'root') persists named Scenario objects to chrome.storage.local['oarm_scenarios']; load() applies via bridge.sendControl + bridge.setCatchMode; exportJson() serialises current mock table to JSON; importJson() applies without saving |
Developers can commit scenario files alongside feature branches and restore a full mock state in one click, or share via JSON export |
| History tab request inspector | payloadSections(ev, meta) extracts path-param names from meta.path {placeholders}, fills in actual arg values to produce GET /pet/42 header line, labels remaining args as Query or Body based on HTTP method, renders [FormData]/[Blob] as badges instead of JSON strings |
Raw JSON.stringify(args) was unreadable for multi-arg calls; path-template parsing gives named labels without any runtime schema |
validateResponses |
Opt-in generator flag; embeds the dereferenced response JSON Schema per endpoint and wires a _validateResponse() function through httpResource's parse hook (using @cfworker/json-schema's Validator class, same as the DevTools panel). OAS nullable: true is rewritten to a type array before embedding. Endpoints with a circular $ref in their response schema are skipped (not thrown) — JSON.stringify/recursive-walk failure is caught and treated as "no validation for this endpoint" |
Compile-time types vanish at runtime; a spec/server drift on a third-party API otherwise surfaces as an unhandled TypeError deep in a component instead of at the httpResource boundary. Kept opt-in because embedding a schema per endpoint has a bundle-size cost |
- Angular 22 release notes: https://angular.dev/events/v22
httpResourcedocs: https://angular.dev/guide/http/http-resourceInjectionTokendocs: https://angular.dev/api/core/InjectionToken- Nx generator guide: https://nx.dev/extending-nx/recipes/local-generators
openapi-typescript: https://openapi-ts.dev@apidevtools/swagger-parser: https://apitools.dev/swagger-parser- Angular Material: https://material.angular.dev
- YouTube Data API v3 spec: https://developers.google.com/youtube/v3
- For navigating/exploring the workspace, invoke the
nx-workspaceskill first - it has patterns for querying projects, targets, and dependencies - When running tasks (for example build, lint, test, e2e, etc.), always prefer running the task through
nx(i.e.nx run,nx run-many,nx affected) instead of using the underlying tooling directly - Prefix nx commands with the workspace's package manager (e.g.,
pnpm nx build,npm exec nx test) - avoids using globally installed CLI - You have access to the Nx MCP server and its tools, use them to help the user
- For Nx plugin best practices, check
node_modules/@nx/<plugin>/PLUGIN.md. Not all plugins have this file - proceed without it if unavailable. - NEVER guess CLI flags - always check nx_docs or
--helpfirst when unsure
- For scaffolding tasks (creating apps, libs, project structure, setup), ALWAYS invoke the
nx-generateskill FIRST before exploring or calling MCP tools
- USE for: advanced config options, unfamiliar flags, migration guides, plugin configuration, edge cases
- DON'T USE for: basic generator syntax (
nx g @nx/react:app), standard commands, things you already know - The
nx-generateskill handles generator discovery internally - don't call nx_docs just to look up generator syntax