ng-openapi-signals
July 15, 2026 · View on GitHub
Signal-first OpenAPI client generator for Angular using resource() and fetch().
ng-openapi-signals generates lightweight Angular API clients from OpenAPI specifications.
GET endpoints are generated as Angular resource() APIs, while mutating endpoints such as POST, PUT, PATCH and DELETE are generated as Promise-based fetch() methods.
Features
- Generate Angular API clients from OpenAPI 3.x specifications
- Signal-first read APIs using Angular
resource() - Lightweight runtime based on native
fetch() - No dependency on Angular
HttpClient(optionalhttpClienttransport available) - Typed models generated from OpenAPI schemas
- Path parameter support
- Query parameter support
- Advanced query parameter serialization (OpenAPI
style/explode:form,spaceDelimited,pipeDelimited,deepObject) - Header parameter support
- JSON request body support
- Multipart form data (
multipart/form-data) and file upload support application/x-www-form-urlencodedrequest body support- Custom request content types
- JSON, text,
Blob,ArrayBufferandReadableStreamresponse handling - File download support
- Fetch middleware (onion-style
(request, next) => response) - Auth header hooks
- Custom default headers
- Custom error mapping
- Request and response hooks
- Base URL configuration via
provideNgOpenapiSignals() - Optional signal-based mutations — reactive
result/error/status/isLoadingsignals for POST/PUT/PATCH/DELETE
Requirements
- Node.js 22 or newer
- Angular 22 or newer
- TypeScript
- OpenAPI 3.x JSON or YAML specification
How to Start
Get up and running in four steps.
1. Install
npm install -D ng-openapi-signals
Or run it directly with npx (no install needed):
npx ng-openapi-signals generate --input openapi.json --output src/generated/api
2. Generate the API client
Create a config file ng-openapi-signals.config.ts in your project root (recommended):
import {defineConfig} from 'ng-openapi-signals/config';
export default defineConfig({
input: './openapi.json',
output: 'src/generated/api',
});
Then run:
ng-openapi-signals generate --config ng-openapi-signals.config.ts
Or generate without a config file:
ng-openapi-signals generate \
--input ./openapi.json \
--output ./src/generated/api
This generates an Angular API client in src/generated/api:
src/generated/api/
api-fetch-client.ts # or api-http-client.ts (depends on transport)
api-error.ts
signal-utils.ts
providers.ts
index.ts
models/
user.ts
create-user-request.ts
index.ts
resources/
users.api.ts
index.ts
3. Configure Angular
Provide the API base URL in your application config:
import {ApplicationConfig} from '@angular/core';
import {provideNgOpenapiSignals} from './generated/api';
export const appConfig: ApplicationConfig = {
providers: [
provideNgOpenapiSignals({
basePath: 'https://api.example.com',
}),
],
};
4. Use the generated API
import {Component, inject, signal} from '@angular/core';
import {UsersApi} from './generated/api';
@Component({
selector: 'app-user-detail',
template: `
@if (user.isLoading()) {
<p>Loading...</p>
}
@if (user.error()) {
<p>Something went wrong.</p>
}
@if (user.hasValue()) {
<h1>{{ user.value().name }}</h1>
}
`,
})
export class UserDetailComponent {
private readonly usersApi = inject(UsersApi);
readonly userId = signal('123');
readonly user = this.usersApi.getUserByIdResource({
id: this.userId,
});
}
That's it — you now have a fully typed, signal-first Angular API client.
CLI Usage
Generate
ng-openapi-signals generate --input <openapi-file> --output <output-directory>
Options
| Option | Description |
|---|---|
-i, --input <path> | Path to the OpenAPI JSON or YAML file |
-o, --output <path> | Output directory for the generated Angular client |
-c, --config <path> | Path to config file (default: ng-openapi-signals.config.ts) |
--clean | Clean output directory before generation (default: true) |
--no-clean | Preserve existing files in output directory |
--group-by <tag|path> | Group APIs by tag or path (default: tag) |
--transport <fetch|httpClient> | HTTP transport (default: fetch) |
--default-query-style <style> | Default query param style: form, spaceDelimited, pipeDelimited, or deepObject |
--default-query-explode <bool> | Default query param explode (true/false) |
--prefer-content-type <type> | Preferred request content type when multiple are offered |
--signal-mutations | Enable signal-based mutation methods (default: false) |
--date-transformer | Convert ISO-8601 date strings in JSON responses to Date objects (default: false) |
--dry-run | Print the files that would be generated without writing to disk |
--check | Verify generated output is up to date (exits 1 on mismatch; for CI) |
--verbose | Show detailed progress and file lists |
CI: verify generated output
Use --check in CI to verify the generated client is up to date:
ng-openapi-signals generate --input ./openapi.json --output ./src/generated/api --check
The command exits with code 1 when generated files are outdated or missing. Stale files (on disk but no longer in the spec) are reported as warnings but do not fail the check.
Preview without writing: --dry-run
ng-openapi-signals generate --input ./openapi.json --output ./src/generated/api --dry-run --verbose
Generates the client in memory and lists the files (path + line count) without touching disk.
Recommended: use a config file
Using a config file keeps your setup reproducible and version-controllable.
// ng-openapi-signals.config.ts
import {defineConfig} from 'ng-openapi-signals/config';
export default defineConfig({
input: './openapi.json',
output: './src/generated/api',
clean: true,
groupBy: 'tag',
});
Then add a script to your package.json:
{
"scripts": {
"generate:api": "ng-openapi-signals generate --config ng-openapi-signals.config.ts"
}
}
Run it with:
npm run generate:api
You can also generate the API client before building your Angular app:
{
"scripts": {
"generate:api": "ng-openapi-signals generate --config ng-openapi-signals.config.ts",
"build": "npm run generate:api && ng build",
"start": "npm run generate:api && ng serve"
}
}
CLI overrides
CLI flags override config file values. Config file values override defaults.
ng-openapi-signals generate \
--input ./openapi.json \
--output ./src/generated/api \
--group-by path
Generated API Style
- GET endpoints → Angular
resource()APIs (accept plain values or signals) - POST / PUT / PATCH / DELETE → Promise-based
fetch()methods - Signal-based mutations (opt-in via
runtime.signalMutations) →${operationId}Mutation()methods returning aMutationwithresult/error/status/isLoadingsignals
// GET — reactive resource
readonly user = this.usersApi.getUserByIdResource({
id: this.userId, // signal or plain value
});
// POST — promise-based mutation (default)
await this.usersApi.createUser({
name: 'John Doe',
email: 'john@example.com',
});
Signal-based mutations (opt-in)
When runtime.signalMutations is enabled, the generator additionally emits
a ${operationId}Mutation() method for every POST/PUT/PATCH/DELETE endpoint,
alongside the existing Promise-based method (strictly additive).
// Signal-based mutation — reactive state, no manual `busy` flag
readonly creating = this.usersApi.createUserMutation();
create(): void {
this.creating.mutate({ name: 'John Doe', email: 'john@example.com' });
}
// In the template:
// creating.isLoading() → boolean signal
// creating.result() → the created user (or undefined)
// creating.error() → the last error (or undefined)
// creating.status() → 'idle' | 'loading' | 'success' | 'error'
// creating.reset() → clears result/error, returns to 'idle'
For endpoints with path/query/header parameters, the parameters are bound
at construction time (captured in the closure), and only the request body
is passed to mutate(body):
readonly uploading = this.usersApi.uploadUserAvatarMutation({
id: this.userId, // signal or plain value
});
upload(): void {
this.uploading.mutate({ file: this.file, caption: 'Profile photo' });
}
Enable the feature via the config file or CLI:
// ng-openapi-signals.config.ts
export default defineConfig({
input: './openapi.json',
output: './src/generated/api',
runtime: { signalMutations: true },
});
ng-openapi-signals generate --signal-mutations
See
RUNTIME.mdfor full details onMaybeSignal<T>, theMutationinterface, response parsing, and more.
Date Transformer
When runtime.dateTransformer is enabled (default false), the generator emits a date-utils.ts runtime file with a recursive transformDates() function that converts ISO-8601 date-time strings (e.g. 2026-07-15T12:00:00Z) found anywhere in a parsed JSON response body into Date instances. Non-JSON responses (text, blob, arrayBuffer, stream) are left untouched.
// ng-openapi-signals.config.ts
import {defineConfig} from 'ng-openapi-signals/config';
export default defineConfig({
input: './openapi.json',
output: './src/generated/api',
runtime: { dateTransformer: true },
});
ng-openapi-signals generate --date-transformer
The transformer is applied automatically inside the generated client's JSON parsing path — no additional setup is needed at runtime. Works with both fetch and httpClient transports.
Example snippets
The repository includes standalone, commented example files in examples/usage/:
resource-usage.ts— GET endpoint withresource()and signalsmutation-usage.ts— POST/PUT/PATCH/DELETE as Promisesmutation-signal-usage.ts— signal-based mutation (runtime.signalMutations)mutation-signal-params-usage.ts— signal-based mutation with path/query/header paramsauth-interceptor.ts— auth headers and fetch middlewarehttp-client-usage.ts—httpClienttransport setupmultipart-upload.ts— file upload withFormDatadate-transform-usage.ts— automatic ISO-8601 date string → Date conversion (runtime.dateTransformer)
These are illustrative only — adjust the import paths to your generated client directory. They are not included in the npm package.
Runtime
The generated client includes a small runtime:
api-fetch-client.ts (or api-http-client.ts)
api-error.ts
signal-utils.ts
mutation-utils.ts (only when runtime.signalMutations is enabled)
date-utils.ts (only when runtime.dateTransformer is enabled)
providers.ts
ApiFetchClient— wraps nativefetch(), handles base URL, JSON/text/Blob responses, query params, abort signals, middleware, hooks, and error mapping.ApiHttpClient— wraps AngularHttpClient(whentransport: 'httpClient'), same feature set, integrates withHttpInterceptors.provideNgOpenapiSignals()— configures the runtime (base URL, headers, auth, middleware, hooks, error mapper).
See
RUNTIME.mdfor the fullprovideNgOpenapiSignals()API and all runtime extension points.
Configuration
ng-openapi-signals supports an optional config file for project-level defaults.
Options
| Option | Type | Default | Description |
|---|---|---|---|
input | string | — | Path to the OpenAPI JSON or YAML file |
output | string | — | Output directory for the generated Angular client |
clean | boolean | true | Clean output directory before generation |
groupBy | 'tag' | 'path' | 'tag' | Group generated APIs by OpenAPI tag or URL path segment |
runtime | RuntimeConfig | {} | Runtime options (see below) |
runtime
| Option | Type | Default | Description |
|---|---|---|---|
transport | 'fetch' | 'httpClient' | 'fetch' | HTTP transport (fetch = native fetch, httpClient = Angular HttpClient) |
defaultHeaders | Record<string, string> | {} | Static default headers baked into provideNgOpenapiSignals defaults |
responseTypeHints | boolean | true | Emit responseType hints in generated methods based on response content |
defaultQueryStyle | 'form' | 'spaceDelimited' | 'pipeDelimited' | 'deepObject' | 'form' | Default query param serialization style when the spec doesn't specify style |
defaultQueryExplode | boolean | true | Default explode flag for query params when the spec doesn't specify it |
preferContentType | string | 'application/json' | Preferred content type when a request body offers multiple media types |
signalMutations | boolean | false | Generate ${operationId}Mutation() methods with reactive signals for POST/PUT/PATCH/DELETE |
dateTransformer | boolean | false | Convert ISO-8601 date-time strings in JSON responses to Date instances (emits date-utils.ts) |
Using the httpClient transport
By default the generated runtime uses native fetch().
To use Angular HttpClient instead (e.g. to integrate with HttpInterceptors), set transport: 'httpClient':
// ng-openapi-signals.config.ts
import {defineConfig} from 'ng-openapi-signals/config';
export default defineConfig({
input: './openapi.json',
output: './src/generated/api',
runtime: {
transport: 'httpClient',
},
});
Or via the CLI:
ng-openapi-signals generate --input ./openapi.json --output ./src/generated/api --transport httpClient
When httpClient is selected:
- The generator emits
ApiHttpClientinstead ofApiFetchClient. provideNgOpenapiSignals()does not includeprovideHttpClient()— register it yourself in your app config (e.g.provideHttpClient(withInterceptors([...]))) so you keep full control over interceptors and their order.- The
NG_OPENAPI_SIGNALS_MIDDLEWAREtoken is not emitted. - Generated API service methods (
resource()loaders, mutations) remain identical — only the underlying client changes.
Grouping
By default, APIs are grouped by OpenAPI tag (groupBy: 'tag').
Each tag becomes one service file: resources/<tag>.api.ts.
Set groupBy: 'path' to group by the first path segment instead.
For example, /users/{id} and /users are grouped into resources/users.api.ts.
Preserving output
Set clean: false to preserve existing files in the output directory:
export default defineConfig({
input: './openapi.json',
output: './src/generated/api',
clean: false,
});
Advanced Request Support
Query Parameter Serialization
The generator supports OpenAPI parameter style and explode for query parameters:
| Style | explode: true | explode: false |
|---|---|---|
form (default) | tags=a&tags=b (repeated) | tags=a,b (comma-separated) |
spaceDelimited | tags=a&tags=b (repeated) | tags=a b (space-separated) |
pipeDelimited | tags=a&tags=b (repeated) | tags=a|b (pipe-separated) |
deepObject | filters[status]=active | — |
Parameters with default style (form + explode: true) are passed as plain values for backward compatibility.
Non-default styles are wrapped with metadata: { value: params.tags, style: 'spaceDelimited', explode: false }.
Multipart Form Data and File Upload
When a request body uses multipart/form-data, the generator emits formData: body instead of body:.
The runtime builds a FormData object from the typed input. Binary parts (format: binary) are typed as Blob.
// OpenAPI: multipart/form-data with file + caption
await this.usersApi.uploadUserAvatar(
{ file: blob, caption: 'Profile photo' },
{ id: 'usr_123' },
);
The runtime automatically:
- Builds
FormDatafrom the typed object - Appends
Blobvalues directly (no JSON serialization) - Lets the browser set the
Content-Typewith the multipart boundary
application/x-www-form-urlencoded
For URL-encoded form bodies, the runtime builds URLSearchParams from the typed object.
Custom Content Types
When a request body uses a non-JSON content type (e.g. application/octet-stream),
the generator emits a contentType field. The runtime passes Blob/ArrayBuffer bodies
through without JSON serialization.
File Download and Stream Responses
Binary responses (image/*, application/octet-stream, etc.) are handled as Blob or ArrayBuffer.
text/event-stream responses are handled as ReadableStream (fetch transport) or Blob (httpClient transport).
Header Parameters
Header parameters (in: header) are generated as method arguments and merged into the request headers object.
Header names with hyphens (e.g. X-Request-Id) are properly quoted in TypeScript.
Current Scope
For the full list of planned features and milestones, see the Roadmap.
For release notes and version history, see the Changelog.
Design Philosophy
ng-openapi-signals follows a simple design:
GET endpoints
→ Angular resource() + fetch()
POST / PUT / PATCH / DELETE endpoints
→ Promise-based fetch() methods
The goal is to generate Angular code that feels natural in modern signal-based applications while keeping the runtime small and easy to understand.
Generated Code
Generated files include this header:
// Auto-generated by ng-openapi-signals.
// Do not edit manually.
Do not manually edit generated files. Change your OpenAPI specification or generator configuration instead.
License
MIT