@seothos/openapi-generator
v0.0.5
Published
Generate Angular code from an OpenAPI specification: TypeScript types, HTTP services, [NgRx Signal Store](https://ngrx.io/guide/signals) stores, signal forms, and ready-to-use Angular Material components — plus an optional throwaway test app to preview ev
Readme
@seothos/openapi-generator
Generate Angular code from an OpenAPI specification: TypeScript types, HTTP services, NgRx Signal Store stores, signal forms, and ready-to-use Angular Material components — plus an optional throwaway test app to preview everything.
You point it at a spec (a URL or an inline JSON object) and a config file, run one command, and get a folder of typed, idiomatic Angular code.
Requirements
The generated code targets a modern Angular app:
- Angular 21+ (
@angular/core,@angular/common/http,@angular/forms/signals) @ngrx/signals21+ — used by the generated stores@angular/material— used by the generated form componentsluxon— only if you setdateType: 'DateTime'(dates becomeDateTime)
@angular/core and @ngrx/signals are declared as peer dependencies, so in a
normal Angular workspace they're already installed.
Install
npm install --save-dev @seothos/openapi-generator
# or: pnpm add -D @seothos/openapi-generator
# or: yarn add -D @seothos/openapi-generatorQuick start
1. Create a config file
The generator reads a config file that default-exports a config object built
with createConfig. Use a .mjs (or .js) file so Node can run it without any
extra tooling.
Loading the spec straight from a running server:
// openapi.config.mjs
import { createConfig } from '@seothos/openapi-generator/generator';
export default createConfig({
url: 'https://api.example.com/openapi.json',
outputDir: './src/app/generated',
});Or from a local spec file:
// openapi.config.mjs
import { readFileSync } from 'node:fs';
import { createConfig } from '@seothos/openapi-generator/generator';
const spec = JSON.parse(readFileSync('./openapi.json', 'utf-8'));
export default createConfig({
json: spec,
outputDir: './src/app/generated',
});TypeScript config
Prefer a typed config? Use openapi.config.ts — you get autocompletion and
type-checking on every option, and you can import the config enums:
// openapi.config.ts
import { readFileSync } from 'node:fs';
import {
createConfig,
TypeGroupingMode,
} from '@seothos/openapi-generator/generator';
const spec = JSON.parse(readFileSync('./openapi.json', 'utf-8'));
export default createConfig({
json: spec,
outputDir: './src/app/generated',
typeGroupingMode: TypeGroupingMode.NO_GROUPING,
dateType: 'DateTime',
fileHeader: `import { DateTime } from 'luxon';`,
});Node can't import .ts directly, so run it through a TypeScript loader such as
tsx:
npx tsx ./node_modules/@seothos/openapi-generator/bin.js openapi.config.ts2. Run the generator
# .mjs / .js config — no extra tooling
npx openapi-generate openapi.config.mjs
# .ts config — via a TypeScript loader
npx tsx ./node_modules/@seothos/openapi-generator/bin.js openapi.config.tsThat's it — the generated code lands in outputDir.
Add it to your package.json so it's repeatable:
{
"scripts": {
"generate:api": "openapi-generate openapi.config.mjs",
"generate:api:ts": "tsx ./node_modules/@seothos/openapi-generator/bin.js openapi.config.ts"
}
}CLI
openapi-generate <config-file-path> [--verbose | --timings | --quiet]| Flag | Output |
| ------------ | ---------------------------------------------------------------- |
| (none) | Basic progress messages and a completion summary. |
| --verbose | Per-step [START]/[DONE] logs with durations. |
| --timings | Timing summary only (⏱️ <step>: XXms) — useful for profiling. |
| --quiet | Errors only — useful in CI. |
What gets generated
Inside outputDir you'll get (each can be toggled off — see below):
| Output | Description |
| -------------- | ----------------------------------------------------------------------- |
| types | types.gen.ts interfaces, plus defaults.ts with default-value seeds. |
| endpoints | Typed endpoint metadata grouped by tag. |
| services | HttpClient-based services, one method per operation. |
| stores | NgRx Signal Store per resource, with call-state and entity helpers. |
| state | Shared state contracts used by the stores. |
| forms | Signal-forms classes with validation derived from the schema. |
| components | Standalone Angular Material form components wired to the stores. |
| schemas | Component schema metadata. |
| test-app | An optional standalone app that routes to every generated component. |
A top-level index.ts re-exports everything, and runtime helpers
(withEntityCollection, CallState, …) are imported from this package.
Using the generated code
Provide the API base URL
Generated services build request URLs from the API_ENV injection token.
Provide it once in your app config:
// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { API_ENV } from '@seothos/openapi-generator';
export const appConfig: ApplicationConfig = {
providers: [
provideHttpClient(),
{ provide: API_ENV, useValue: { production: false, apiUrl: 'https://api.example.com' } },
],
};Use a generated store or component
Names are derived from the operation and tag, so the exact symbols depend on
your spec. For an operation like POST /account-holders you'd get something
like:
import { Component, inject } from '@angular/core';
import {
PostCreateAccountHolderComponent,
AccountHoldersStore,
} from './generated';
@Component({
selector: 'app-root',
imports: [PostCreateAccountHolderComponent],
template: `
<app-post-create-account-holder-component
[showSnackbar]="false"
(submitSuccess)="onCreated($event)"
/>
`,
})
export class App {
protected readonly store = inject(AccountHoldersStore);
onCreated(result: unknown) {
console.log('created', result);
}
}Generated components emit submitSuccess, submitError, and formCancel
outputs, and expose a showSnackbar input (default true) so you can suppress
the built-in Material snackbars and drive your own notifications instead.
Extending a component with your own template
The generated component ships with a default Material template, but the form
plumbing lives in the TypeScript class — formInstance, the onSubmit() /
onCancel() handlers, isSubmitting(), the hasError() / getErrorMessage()
helpers, the injected store, and the inputs/outputs. Those members are
protected, so you can subclass the generated component and supply your own
template while reusing all of that logic. Nothing needs to be reimplemented —
you only swap the markup.
import { Component } from '@angular/core';
import { FormField } from '@angular/forms/signals';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatButtonModule } from '@angular/material/button';
import { PostCreateAccountHolderComponent } from './generated';
@Component({
selector: 'app-account-holder-form',
imports: [FormField, MatFormFieldModule, MatInputModule, MatButtonModule],
// Your markup, bound to the inherited (protected) members:
template: `
<form (ngSubmit)="onSubmit()">
<mat-form-field>
<mat-label>Name</mat-label>
<input matInput [formField]="formInstance.form.name" />
@if (hasError('name')) {
<mat-error>{{ getErrorMessage('name') }}</mat-error>
}
</mat-form-field>
<!-- …the rest of your fields… -->
<button mat-raised-button type="submit" [disabled]="isSubmitting()">
Save
</button>
</form>
`,
})
export class AccountHolderForm extends PostCreateAccountHolderComponent {}Because you're subclassing, the initialValue / showSnackbar inputs and the
submitSuccess / submitError / formCancel outputs are inherited too — use
the component exactly like the generated one, just with your markup.
Configuration reference
Pass these to createConfig. Only outputDir and one of url / json are
required; everything else has a sensible default.
Source (required — pick one)
| Option | Type | Description |
| ------ | -------------- | -------------------------------------------- |
| url | string | Fetch the OpenAPI spec from this URL. |
| json | OpenApiSpec | Use an already-loaded spec object. |
Common options
| Option | Type | Default | Description |
| ------------------------ | --------- | ----------------------------- | -------------------------------------------------------------------------------------------- |
| outputDir | string | (required) | Directory the generated code is written to. |
| clearDirectory | boolean | false | Empty outputDir before writing. |
| dateType | string | 'string' | TS type for date/date-time fields, e.g. 'DateTime' (luxon) or 'Date'. |
| fileHeader | string | '' | Text prepended to generated files — handy for imports your dateType needs. |
| useReadOnlyArrays | boolean | true | Emit readonly T[] for array properties. |
| typeGroupingMode | enum | NO_GROUPING | How generated types are grouped into files. |
| servicePrefixPathMatch | string | '' | Endpoints whose path contains this string go into a separate, prefixed service (e.g. external → ExternalCustomersService). |
| devtools | boolean | false | Compose withDevtools() into each store for Redux DevTools visibility. |
| runtimePackage | string | '@seothos/openapi-generator'| Package the generated code imports runtime helpers from. Change only if you re-publish them. |
Output toggles
All write* options default to true; set to false to skip that output.
writeTypes, writeEndpoints, writeServices, writeStores, writeState,
writeForms, writeComponents, writeComponentSchemas, writeTypeMapping,
writeTestApp.
export default createConfig({
url: 'https://api.example.com/openapi.json',
outputDir: './src/app/generated',
dateType: 'DateTime',
fileHeader: `import { DateTime } from 'luxon';`,
writeTestApp: false, // skip the preview app
});Generated test app
When writeTestApp is enabled (the default), the generator also fills in a
standalone Angular app that routes to every generated form component — a
quick way to click through all your forms without wiring anything up yourself.
It writes app.routes.ts, app.ts, app.html, app.config.ts, and styles,
with one route per operation that has a request body, plus a fallback report
route listing any schema fields it couldn't map to a control.
The generator does not scaffold the Angular app for you — it only fills an app that already exists. Create one first (nx, the Angular CLI, etc.), then re-run the generator. If the target directory isn't there, the test app step is skipped with a hint instead of writing orphan files.
The routes import the generated components by name, so the test app needs to
resolve them through generatedPackage — set this to whatever specifier your
generated library is exposed as.
| Option | Type | Default | Description |
| ----------------- | --------- | ----------------------------------- | --------------------------------------------------------------------------- |
| writeTestApp | boolean | true | Generate the test app (skipped if the target app dir doesn't exist). |
| testAppDir | string | sibling generated-test-app/src/app| The app's src/app directory. Defaults to a path derived from outputDir. |
| generatedPackage| string | '@frontend-toolbox/generated' | Import specifier the test app uses to import the generated components. |
export default createConfig({
url: 'https://api.example.com/openapi.json',
outputDir: './src/app/generated',
writeTestApp: true,
testAppDir: './apps/api-playground/src/app',
generatedPackage: '@my-org/generated-api',
});Programmatic use
The CLI is a thin wrapper around the exported main(). You can also run the
whole pipeline yourself:
import { main } from '@seothos/openapi-generator/generator';
// reads process.argv: [node, script, <config-path>, ...flags]
await main();@seothos/openapi-generator/generator also exports createConfig, the default
generators/writers, and the config types, so you can swap in custom
implementations of any stage (loader, type generator, output writers, …).
