npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@seothos/openapi-generator

v0.1.0

Published

Generate Angular code from an OpenAPI spec: TypeScript types, HTTP services, NgRx Signal Store stores, signal forms, and native-HTML form components (Angular Material skin optional).

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 form components — plus an optional throwaway test app to preview everything.

All generated UI is native semantic HTML by default (no UI-kit dependency): form components, filter bars, and paginated list views. Prefer Angular Material? Opt in per writer:

import {
  MaterialComponentOutputWriter,
  MaterialFilterComponentOutputWriter,
  MaterialListViewOutputWriter,
} from '@seothos/openapi-generator/generator';

createConfig({
  // …
  componentOutputWriter: new MaterialComponentOutputWriter(),
  filterComponentOutputWriter: new MaterialFilterComponentOutputWriter(),
  listViewOutputWriter: new MaterialListViewOutputWriter(),
});

The Material skins additionally need @angular/material installed, a theme, and — for the filter datepickers — the date-adapter provider matching your date library (e.g. provideLuxonDateAdapter() from @angular/material-luxon-adapter) in your app config. The Material list views use cdk virtual scrolling (infinite scroll); the native default paginates instead.

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/signals 21+ — only for the opt-in storeEngine: 'ngrx' (an optional peer); the default 'signals' engine emits plain classes with no ngrx dependency
  • @angular/material — only for the opt-in Material skins; the default native output has no UI-kit dependency at all
  • luxon — only if you set dateType: 'DateTime' (dates become DateTime)

@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-generator

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

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

That'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 form components wired to the stores (native HTML by default, Material opt-in). | | 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
      [showNotifications]="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 notifications toggle (default true) so you can suppress the built-in feedback and drive your own notifications instead: the native components announce results in an inline aria-live region behind showNotifications; the Material skin uses MatSnackBar behind showSnackbar.

On a failed submission, server-side validation errors are mapped onto the form fields automatically: the component's submit() action runs the response through serverErrorsToFieldErrors (exported from the generated utilities), which understands the common API shapes — { errors: { fieldPath: message } }, { errors: [{ field, message }] }, and single { field, message } — and each error displays under the field it names (dotted paths reach nested fields; anything unmapped lands on the form root). The errors clear when the user edits the field.

Extending a component with your own template

The generated component ships with a default 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 / notification inputs and the submitSuccess / submitError / formCancel outputs are inherited too — use the component exactly like the generated one, just with your markup.

Emitted contracts (for custom writers)

Every writer is pluggable (componentOutputWriter, formOutputWriter, … options on createConfig; each abstract base has a single write* method the orchestrator awaits). The generated outputs reference each other by name, so a from-scratch writer must honor these shapes — or replace the writers that consume them too:

  • Stores (stores/): per tag, ${Tag}Store (entity) and ${Tag}ListStore (collection). Every operation is one method named after the PascalCase operationId: store.${PascalOperationId}({ ...pathParams, body, queryParams }) returns a cold Observable of the typed response — subscribing triggers the call, store state (item/collection + call state) updates on success, and errors reach the subscriber's error channel. Fire-and-forget is .subscribe(); components bridge to signal-forms' submit() with firstValueFrom at that one framework boundary.
  • Forms (forms/): ${PascalOperationId}Formform (the signal-forms FieldTree), isValid(), isDirty(), getValue(), setValue(v), patchValue(v), reset(). Bulk (array-body) forms add master-detail state: selectedIndex(), addItem(), removeItem(i), selectItem(i). A bulk endpoint that accepts a single entity also gets ${PascalOperationId}SingleForm.
  • Endpoints (endpoints/): query-params type ${PascalOperationId}Params and a ${PascalOperationId}ParamsDefault seed.
  • Defaults (defaults/): ${TypeName}Defaults per named body type — used to seed forms and new array rows.
  • Naming: snake_case operationIds become PascalCase; file/selector names are the kebab-case of the class name. The endpoint model hands writers ep.storeName, ep.bodyModel, ep.body.isArray, ep.collectionMutation?.acceptsSingle etc. — writers should consume those rather than re-deriving.

To re-skin the built-in component pipeline instead of replacing it, extend DefaultComponentOutputWriter (native HTML) or MaterialComponentOutputWriter and override its protected seam methods (grep SKIN SEAM in the source).

Material-free by default

Every default writer emits native HTML — a workspace without @angular/material can use the full output (forms, filters, list views, test app) with no toggles. Only the opt-in Material* writers reintroduce the Material dependency.

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. externalExternalCustomersService). | | devtools | boolean | false | Compose withDevtools() into each store for Redux DevTools visibility. | | listClassifier | (endpoint) => boolean \| undefined | (heuristic) | Which endpoint is a tag's list (paginated collection store). false = never a list (demoted to a single-value slot); true = the tag's primary list (first true wins — a tag's list store hosts exactly one collection); undefined = default heuristic (first paginated-wrapper GET per tag, in spec order). Receives the extracted model (PascalCase operationIds). | | storeEngine | 'signals' \| 'ngrx' | 'signals' | Store engine for the (coupled) state + store writers. signals emits flat @Injectable classes on plain Angular signals — no ngrx dependency, extendable via class MyStore extends UsersListStore. ngrx emits the signalStore-based output (needs the @ngrx/signals peer; required for devtools). Both engines expose the identical public store surface, so switching is config-only. | | listMode | 'accumulate' \| 'replace' \| (endpoint) => mode | 'accumulate' | How list stores treat loaded pages. accumulate appends into the live collection (infinite-scroll-friendly; the native list view paginates over it client-side). replace keeps only the current page (the native view pages server-side via loadPage(offset)). A function decides per endpoint — note it receives the extracted model, where operationIds are PascalCase. The Material list-view skin supports accumulate only. | | 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, writeFilters, writeListViews, writeComponentSchemas, writeTypeMapping, writeTestApp.

writeComponents emits a request-body form component per write endpoint. writeFilters emits a filter-bar component per list (paginated GET) endpoint that has query filters — a header of controls for the list query, driving the list store's filter() method. Each filter component emits its assembled filters via a filtersChanged output (wire it to the store), supports a live input (debounced auto-emit, hides the Apply/Clear buttons), a hiddenFilters array input to hide individual filter fields, and an additionalFilters input for filters supplied from outside the bar (merged into every emit and re-emitted when they change — e.g. constrain a list to the current parent entity):

<app-list-customers-filter
  [live]="true"
  [hiddenFilters]="['status']"
  [additionalFilters]="{ customer__eq: customerId() }"
  (filtersChanged)="store.filter($event)" />

writeListViews emits an infinite-scrolling list-view component per list (paginated GET) endpoint, plus a set of reusable ui-table primitives (a CDK-virtual-scroll table with Material sortable headers) emitted once into list-views/ui-table/. Each view renders one column per scalar field of the list's item type and is emits-only: pass the list store's items()/total() in, and wire scrolled to loadMore() (fired as the user nears the end) and sortChange to sortBy():

<app-list-customers-list-view
  [items]="store.items()"
  [total]="store.total()"
  (scrolled)="store.loadMore()"
  (sortChange)="store.sortBy($event)" />
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. When writeFilters is on, it also adds a Filters nav section: one route per list endpoint with a demo host (filter-demos.ts) that wires the filter bar's filtersChanged output to the list store's filter() and shows the loaded item count. When writeListViews is on, a Lists nav section does the same for the list views: a demo host (list-view-demos.ts) injects each list store, loads the first page, and wires the view's scrolled/sortChange outputs to loadMore()/sortBy().

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.

Two one-time additions the generator doesn't make to the app shell: a Material theme @import in src/styles.css, and — for the filter bars' mat-icons — the Material Icons font in src/index.html:

<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />

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, …).