An open API service indexing awesome lists of open source software.

https://github.com/ynnckrkn/ng-openapi-signals

Signal-first OpenAPI client generator for Angular using `resource()` and `fetch()`.
https://github.com/ynnckrkn/ng-openapi-signals

Last synced: 11 days ago
JSON representation

Signal-first OpenAPI client generator for Angular using `resource()` and `fetch()`.

Awesome Lists containing this project

README

          

# ng-openapi-signals

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` (optional `httpClient` transport 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-urlencoded` request body support
- Custom request content types
- JSON, text, `Blob`, `ArrayBuffer` and `ReadableStream` response 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`/`isLoading` signals 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

```bash
npm install -D ng-openapi-signals
```

Or run it directly with `npx` (no install needed):

```bash
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):

```ts
import {defineConfig} from 'ng-openapi-signals/config';

export default defineConfig({
input: './openapi.json',
output: 'src/generated/api',
});
```

Then run:

```bash
ng-openapi-signals generate --config ng-openapi-signals.config.ts
```

Or generate without a config file:

```bash
ng-openapi-signals generate \
--input ./openapi.json \
--output ./src/generated/api
```

This generates an Angular API client in `src/generated/api`:

```text
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:

```ts
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

```ts
import {Component, inject, signal} from '@angular/core';
import {UsersApi} from './generated/api';

@Component({
selector: 'app-user-detail',
template: `
@if (user.isLoading()) {

Loading...


}

@if (user.error()) {

Something went wrong.


}

@if (user.hasValue()) {

{{ user.value().name }}


}
`,
})
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

```bash
ng-openapi-signals generate --input --output
```

### Options

| Option | Description |
| --------------------------------- | ------------------------------------------------------------- |
| `-i, --input ` | Path to the OpenAPI JSON or YAML file |
| `-o, --output ` | Output directory for the generated Angular client |
| `-c, --config ` | 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 ` | Group APIs by tag or path (default: tag) |
| `--transport ` | HTTP transport (default: fetch) |
| `--default-query-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:

```bash
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`

```bash
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.

```ts
// 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`:

```json
{
"scripts": {
"generate:api": "ng-openapi-signals generate --config ng-openapi-signals.config.ts"
}
}
```

Run it with:

```bash
npm run generate:api
```

You can also generate the API client before building your Angular app:

```json
{
"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.

```bash
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 a `Mutation` with `result`/`error`/`status`/`isLoading` signals

```ts
// 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).

```ts
// 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)`:

```ts
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:

```ts
// ng-openapi-signals.config.ts
export default defineConfig({
input: './openapi.json',
output: './src/generated/api',
runtime: { signalMutations: true },
});
```

```bash
ng-openapi-signals generate --signal-mutations
```

> See [`RUNTIME.md`](./RUNTIME.md) for full details on `MaybeSignal<T>`, the `Mutation` interface, 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.

```ts
// 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 },
});
```

```bash
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/`](./examples/usage/):

- `resource-usage.ts` — GET endpoint with `resource()` and signals
- `mutation-usage.ts` — POST/PUT/PATCH/DELETE as Promises
- `mutation-signal-usage.ts` — signal-based mutation (`runtime.signalMutations`)
- `mutation-signal-params-usage.ts` — signal-based mutation with path/query/header params
- `auth-interceptor.ts` — auth headers and fetch middleware
- `http-client-usage.ts` — `httpClient` transport setup
- `multipart-upload.ts` — file upload with `FormData`
- `date-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:

```text
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 native `fetch()`, handles base URL, JSON/text/Blob responses, query params, abort signals, middleware, hooks, and error mapping.
- **`ApiHttpClient`** — wraps Angular `HttpClient` (when `transport: 'httpClient'`), same feature set, integrates with `HttpInterceptors`.
- **`provideNgOpenapiSignals()`** — configures the runtime (base URL, headers, auth, middleware, hooks, error mapper).

> See [`RUNTIME.md`](./RUNTIME.md) for the full `provideNgOpenapiSignals()` 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'`:

```ts
// 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:

```bash
ng-openapi-signals generate --input ./openapi.json --output ./src/generated/api --transport httpClient
```

When `httpClient` is selected:

- The generator emits `ApiHttpClient` instead of `ApiFetchClient`.
- `provideNgOpenapiSignals()` does **not** include `provideHttpClient()` — 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_MIDDLEWARE` token 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:

```ts
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`.

```ts
// OpenAPI: multipart/form-data with file + caption
await this.usersApi.uploadUserAvatar(
{ file: blob, caption: 'Profile photo' },
{ id: 'usr_123' },
);
```

The runtime automatically:
- Builds `FormData` from the typed object
- Appends `Blob` values directly (no JSON serialization)
- Lets the browser set the `Content-Type` with 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](./ROADMAP.md).
For release notes and version history, see the [Changelog](./CHANGELOG.md).

## Design Philosophy

`ng-openapi-signals` follows a simple design:

```text
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:

```ts
// 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