惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

月光博客
月光博客
Cyberwarzone
Cyberwarzone
L
LINUX DO - 最新话题
N
News and Events Feed by Topic
T
Troy Hunt's Blog
Help Net Security
Help Net Security
S
Security @ Cisco Blogs
Google DeepMind News
Google DeepMind News
Security Archives - TechRepublic
Security Archives - TechRepublic
M
MIT News - Artificial intelligence
G
Google Developers Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V2EX - 技术
V2EX - 技术
Y
Y Combinator Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
大猫的无限游戏
大猫的无限游戏
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Microsoft Security Blog
Microsoft Security Blog
Cisco Talos Blog
Cisco Talos Blog
T
Threatpost
Recent Commits to openclaw:main
Recent Commits to openclaw:main
S
SegmentFault 最新的问题
I
InfoQ
H
Hacker News: Front Page
D
Docker
Scott Helme
Scott Helme
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Blog — PlanetScale
Blog — PlanetScale
人人都是产品经理
人人都是产品经理
博客园 - 叶小钗
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
N
Netflix TechBlog - Medium
AWS News Blog
AWS News Blog
Know Your Adversary
Know Your Adversary
博客园 - 【当耐特】
T
Tor Project blog
U
Unit 42
H
Heimdal Security Blog
Microsoft Azure Blog
Microsoft Azure Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Privacy & Cybersecurity Law Blog
PCI Perspectives
PCI Perspectives
美团技术团队
O
OpenAI News
T
Tailwind CSS Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
B
Blog
GbyAI
GbyAI
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
MyScale Blog
MyScale Blog

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Stop Writing Angular HTTP Services by Hand
Konstantin · 2026-06-19 · via DEV Community

Generate signal-native, tree-shakeable API clients from any OpenAPI spec — in one command.


You have an OpenAPI spec. Your Angular app needs to call those endpoints. What do you do?

The usual answer is one of these:

  • Write HttpClient calls by hand in a service, duplicating type information already in the spec
  • Use a generic codegen tool that emits a 2,000-line service class full of endpoints you'll never call
  • Spend a sprint wiring up interceptors, managing base URLs, and keeping types in sync

There's a better way. @constantant/openapi-resource-gen is an Nx generator that reads your OpenAPI 3.x spec and emits one typed InjectionToken per endpoint — each in its own file, each powered by Angular 22's httpResource(). You only bundle what you actually inject.

Requirements: Angular 22+ · Nx 22+


Why one token per endpoint?

The key insight is about tree-shaking. esbuild (Angular's bundler) tree-shakes at file boundaries. If find-pets-by-status.token.ts is never imported, the entire file — including its types and URL logic — costs zero bytes in your production bundle.

A traditional service class is one file with 50 methods. Import it once anywhere and you get all 50. With one token per file, you get only the endpoints you inject.

There's a second benefit: each generated token is a factory that returns an httpResource(). That means your HTTP calls are signal-native from the start — reactive params, automatic re-fetching on signal changes, .value() / .isLoading() / .error() signals — no RxJS, no subscriptions, no manual change detection.


Before and after

Here's the pattern most Angular developers write today:

// ❌ The old way: a hand-written service
@Injectable({ providedIn: 'root' })
export class PetService {
  private http = inject(HttpClient);
  private base = 'https://petstore3.swagger.io/api/v3';

  findByStatus(status: 'available' | 'pending' | 'sold') {
    return this.http.get<Pet[]>(`${this.base}/pet/findByStatus`, {
      params: { status },
    });
  }
}

// Component has to manage an Observable, a subscription, loading state, error state...
@Component({ ... })
export class PetsPageComponent {
  private petService = inject(PetService);
  pets = signal<Pet[]>([]);
  loading = signal(false);
  error = signal<string | null>(null);
  status = signal<'available' | 'pending' | 'sold'>('available');

  constructor() {
    effect(() => {
      this.loading.set(true);
      this.petService.findByStatus(this.status()).subscribe({
        next: (data) => { this.pets.set(data); this.loading.set(false); },
        error: (err) => { this.error.set(err.message); this.loading.set(false); },
      });
    });
  }
}

Problems with this pattern:

  • Types are hand-written and will drift from the spec
  • Loading/error state is boilerplate you write every time
  • Effect + subscribe combination has subtle cancellation bugs
  • The service is never tree-shaken — every endpoint ships, used or not

Here's the generated version:

// ✅ The generated way: inject and call
@Component({ ... })
export class PetsPageComponent {
  private findPetsByStatus = inject(FIND_PETS_BY_STATUS);

  readonly status = signal<'available' | 'pending' | 'sold'>('available');

  // Re-fetches automatically whenever status() changes
  readonly pets = this.findPetsByStatus(() => ({ status: this.status() }));
}

@if (pets.isLoading()) {
  <mat-progress-bar mode="indeterminate" />
}

@for (pet of pets.value() ?? []; track pet.id) {
  <p>{{ pet.name }} — {{ pet.status }}</p>
}

@if (pets.error()) {
  <p class="error">Something went wrong</p>
}

The loading state, error state, and reactive re-fetching come for free. The types come from the spec. Nothing is hand-written.


Setup walkthrough

Step 1 — install

npm install -D @constantant/openapi-resource-gen

Step 2 — generate

Point the generator at a local file or any HTTPS URL — no curl pre-step needed:

npx nx g @constantant/openapi-resource-gen:api-resource \
  --specPath=https://petstore3.swagger.io/api/v3/openapi.yaml \
  --outputDir=libs/petstore-data-access/src \
  --baseUrlToken=PETSTORE_BASE_URL \
  --includeMocks \
  --specId=petstore

That's it. The generator downloads the spec, validates it, runs openapi-typescript to produce a schema.d.ts, dereferences all $ref chains with @apidevtools/swagger-parser, and emits one token file per endpoint.

Step 3 — what you get

libs/petstore-data-access/src/
  schema.d.ts                     ← openapi-typescript output — never touch this
  api-base-url.token.ts           ← InjectionToken<string> for the base URL
  index.ts                        ← barrel — re-exports everything

  pet/
    find-pets-by-status.token.ts  ← one file per endpoint
    add-pet.token.ts
    update-pet.token.ts
    delete-pet.token.ts
    get-pet-by-id.token.ts
    ...

  store/
    ...

  user/
    ...

Endpoints are grouped into subfolders by OpenAPI tag. Each folder gets its own index.ts barrel. The root barrel re-exports everything.

Step 4 — wire up providers

// app.config.ts
import { provideHttpClient } from '@angular/common/http';
import {
  PETSTORE_BASE_URL,
  provideFindPetsByStatus,
  provideGetPetById,
} from '@myapp/petstore-data-access';

export const appConfig: ApplicationConfig = {
  providers: [
    provideHttpClient(),
    { provide: PETSTORE_BASE_URL, useValue: 'https://petstore3.swagger.io/api/v3' },
    provideFindPetsByStatus(),
    provideGetPetById(),
    // Only register tokens you actually use — everything else is tree-shaken away
  ],
};

Step 5 — inject and use

@Component({ ... })
export class PetsPageComponent {
  private findPetsByStatus = inject(FIND_PETS_BY_STATUS);

  readonly status = signal<'available' | 'pending' | 'sold'>('available');
  readonly pets = this.findPetsByStatus(() => ({ status: this.status() }));
}

Step 6 — declare a generate target for easy re-runs

When your spec changes, you want to re-run the generator without remembering the full command. Add a target to project.json:

{
  "targets": {
    "generate": {
      "executor": "@constantant/openapi-resource-gen:generate",
      "options": {
        "specPath": "https://petstore3.swagger.io/api/v3/openapi.yaml",
        "outputDir": "libs/petstore-data-access/src",
        "baseUrlToken": "PETSTORE_BASE_URL",
        "includeMocks": true,
        "specId": "petstore"
      }
    }
  }
}

Now regeneration is just:

npx nx run petstore-data-access:generate

The generator tracks stale files and deletes any .token.ts that no longer corresponds to an endpoint in the spec. No phantom exports accumulate.


The generated token in detail

Every generated file follows the same structure. Here's a GET endpoint with query params:

// 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';

// Types sourced directly from the spec — zero hand-written interfaces
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 any,
          };
        });
    },
  };
}

Three things worth noting in this generated code:

Block-body reactive lambda, not shorthand. The lambda uses () => { ... return { url } } rather than () => ({ url }). This is deliberate — a shorthand arrow always returns an object, so httpResource always fires. The block form can return undefined to suppress the request entirely, keeping the resource idle.

Types sourced from paths[...]. The query param type and response type both come from the generated schema.d.ts. If your spec changes and you regenerate, the types update automatically. There's no separate interface to maintain.

inject() inside the factory, not in a constructor. The token is fully compatible with Angular's tree-shaking rules — it can be providedIn: 'root' with no side effects on bundle size for tokens that are never injected.


Patterns you'll use every day

Reactive params — re-fetch when a signal changes

readonly statusFilter = signal<'available' | 'pending' | 'sold'>('available');

// Automatically re-fetches when statusFilter() changes
readonly pets = this.findPetsByStatus(() => ({ status: this.statusFilter() }));

No switchMap. No takeUntilDestroyed. The reactive lambda inside httpResource re-runs whenever a signal it reads changes — the same mechanism Angular uses for computed().

Conditional requests — suppress until conditions are met

readonly query = signal('');
readonly apiKey = signal<string | null>(null);

// No HTTP request fires until both query and apiKey are truthy
readonly results = this.youtubeSearch(() =>
  this.query() && this.apiKey()
    ? { q: this.query(), key: this.apiKey()!, part: 'snippet', maxResults: 10 }
    : undefined
);

When the thunk returns undefined, the resource stays in 'idle'.isLoading() is false, .value() is undefined. When the signals change so the thunk returns a value, the first request fires automatically.

Path params

Path params (/pet/{petId}) become required positional arguments:

// Inject by petId input
readonly pet = this.getPetById(this.petId);

// Or as a reactive thunk when you need to combine with query params
readonly pet = this.getPetById(() => this.petId());

Mutations (POST / PUT / DELETE)

Mutation tokens accept a body. Pass a signal for reactive mutations, or use a thunk to suppress until the user submits:

readonly pendingPet = signal<AddPetBody | null>(null);

// No request until user clicks Submit
readonly result = this.addPet(() => this.pendingPet() ?? undefined);

onSubmit(pet: AddPetBody): void {
  this.pendingPet.set(pet);
}

Security — signals all the way down

When the spec declares a bearerAuth or oauth2 scheme, the generator emits an InjectionToken<Signal<string | null>>. Every endpoint token injects it optionally and reads the signal inside its reactive lambda — changing the auth token automatically re-fires all resources that use it.

// oauth2.security-token.ts (generated)
export const OAUTH2 = new InjectionToken<Signal<string | null>>('OAUTH2');

// In your auth logic:
onLoginSuccess(accessToken: string): void {
  this.token.set(accessToken);
  // Every resource that reads OAUTH2 re-fires automatically
}

No interceptor reloads, no manual refresh() calls.

Sharing a resource across components

Each call to the factory creates an independent httpResource. To share one resource across multiple components, hoist it into a root-scoped store token:

export const PETS_STORE = new InjectionToken('PETS_STORE', {
  providedIn: 'root',
  factory: () => {
    const findPetsByStatus = inject(FIND_PETS_BY_STATUS);
    const statusFilter = signal<'available' | 'pending' | 'sold'>('available');
    return {
      statusFilter,
      pets: findPetsByStatus(() => ({ status: statusFilter() })),
    };
  },
});

One HTTP request, shared across every component that injects PETS_STORE.


Testing without infrastructure

The companion package @constantant/openapi-resource-mocks ships a /testing sub-entry designed for Vitest and Jasmine component tests. It does not set up a mock bus, touch window, or emit DOM events — it replaces a token's factory with a signal-based mock and gives you assertion helpers.

import { mockResource } from '@constantant/openapi-resource-mocks/testing';

describe('PetsPageComponent', () => {
  let petsMock: ReturnType<typeof mockResource<FindPetsByStatusResponse>>;

  beforeEach(() => {
    petsMock = mockResource(FIND_PETS_BY_STATUS);

    TestBed.configureTestingModule({
      imports: [PetsPageComponent],
      providers: [petsMock], // MockResourceHandle is a FactoryProvider — drop it in directly
    });
  });

  it('renders pets when data arrives', async () => {
    const fixture = TestBed.createComponent(PetsPageComponent);
    fixture.detectChanges();

    petsMock.ref.resolve([{ id: 1, name: 'Rex', status: 'available', photoUrls: [] }]);
    fixture.detectChanges();

    const rows = fixture.nativeElement.querySelectorAll('.pet-row');
    expect(rows.length).toBe(1);
  });

  it('shows loading skeleton', async () => {
    petsMock = mockResource(FIND_PETS_BY_STATUS, { loading: true });
    // ...
  });

  it('shows error state', async () => {
    petsMock = mockResource(FIND_PETS_BY_STATUS, { error: new Error('503') });
    // ...
  });
});

You can also test multi-step scenarios — pagination, retry-then-succeed, error-then-recover — using a sequence:

petsMock = mockResource(FIND_PETS_BY_STATUS, {
  sequence: [
    { error: new Error('timeout') },    // first call fails
    { error: new Error('timeout') },    // retry also fails
    { value: [{ id: 1, name: 'Rex', ... }] }, // third succeeds
  ],
});


The DevTools extension — live mock control without code changes

Generate with --includeMocks and you unlock something beyond testing: the OpenAPI Resource Mocks DevTools Chrome extension.

Install it (currently via Load unpacked while the Chrome Web Store review completes):

git clone https://github.com/constantant/angular-openapi-gen.git
cd angular-openapi-gen && npm ci
npx nx run openapi-resource-devtools:build
# Then: chrome://extensions → Developer mode → Load unpacked → dist/tools/openapi-resource-devtools/

Your app needs provideMockResourceBus() in its providers — typically in a mock build configuration you run locally:

// app.config.mock.ts
import { provideMockResourceBus } from '@constantant/openapi-resource-mocks';
import * as mocks from '@myapp/petstore-data-access/mock';

export const mockAppConfig: ApplicationConfig = {
  providers: [
    provideMockResourceBus(),
    mocks.provideFindPetsByStatusMock(),
    mocks.provideAddPetMock(),
  ],
};

Open Chrome DevTools on the running app and an API Mocks tab appears. From there you can:

Control mock state without touching code. Click Resolve, Fail, or Loading on any token row. Set a delay. Reset to idle. All without a page reload.

Catch requests before they resolve. Enable catch mode on a token and the next request from that component is held in CAUGHT state. The Respond tab shows exactly what params the component sent. Fill in a response JSON and click Release. This is the fastest way to test loading skeletons and error states for deeply conditional UI.

Import your spec for schema-aware features. Import mocks.manifest.json (generated alongside your tokens) or the full OpenAPI YAML/JSON. With the spec loaded, the Respond tab gains ⚡ Example (generates a valid random payload from the response schema) and ✓ Validate (checks your JSON against the schema before releasing).

Save and load scenarios. Snapshot the entire mock table state — values and catch modes — as a named scenario. Share the JSON with teammates so anyone can reproduce the exact UI state you're reviewing. Commit it alongside a PR to make bug reproductions reproducible.

Create local mocks before the code exists. The + New mock button lets you create a panel-managed entry for an operation you haven't implemented yet. Configure its catch mode and response while you write the Angular component. When provideMockResource() is eventually registered by the app, the local entry is promoted in-place — your configuration carries over.

One particularly useful detail: catch mode survives page navigation. The background service worker injects window.__oarmPendingCatch__ into the page before Angular's module scripts execute, so the very first request during bootstrap is caught — not just user-triggered ones.


Generator options reference

Option Default Description
specPath required Local path or https:// URL to an OpenAPI 3.x spec
outputDir required Where to write the generated files
baseUrlToken API_BASE_URL Name of the base-URL injection token
tagFilter all tags Comma-separated tags to include
namingConvention kebab kebab or camel filenames
providedIn none none (use provideX() helpers) or root
includeMocks false Emit .mock.ts providers and mocks.manifest.json
includeMswHandlers false Emit MSW 2.x handler files (.msw.ts)
specId derived Identifier embedded in mock metadata; must match when importing into the DevTools panel
verbose false Print a +/~/- summary of created/updated/deleted files

How it compares to the alternatives

There are four established tools Angular teams reach for when generating API clients from an OpenAPI spec. Here's how they each work — and where the trade-offs land.

Quick reference

OpenAPI Generator NSwag orval hey-api/openapi-ts openapi-resource-gen
Output unit Service class per tag Service class per tag Service class per tag Flat SDK functions Token per endpoint
Angular HTTP primitive Observable Observable Observable (+ optional httpResource) fetch / XHR httpResource (first-class)
Tree-shaking ❌ Class-level ❌ Class-level ❌ Class-level ⚠️ Flat SDK only File-level (per endpoint)
Type source Generated interfaces Generated interfaces Generated interfaces Generated types paths[] from openapi-typescript
Reactive params Manual (switchMap) Manual Manual Manual Signal tracking, auto-refetch
Signal-based auth Auto-refires on token change
Built-in mocks ⚠️ MSW handlers Bus + /testing + DevTools
Angular-first ⚠️ Multi-language ⚠️ .NET-first ⚠️ Multi-framework ❌ Framework-agnostic Angular-only
OAS version 2.0 + 3.x 2.0 + 3.x 2.0 + 3.x 3.x 3.x

OpenAPI Generator — @openapitools/openapi-generator-cli

The most widely used tool in the ecosystem, with a typescript-angular template that emits one @Injectable({ providedIn: 'root' }) service per API tag. Each method returns an Observable<T>.

The core problem for modern Angular apps is that class-based services are not tree-shakeable at the endpoint level. A long-standing GitHub issue (#11654) tracks exactly this: the angular template bundles every generated method regardless of which ones are actually called. For a GitHub or YouTube API with hundreds of operations, that means a lot of dead code in production.

The second problem is the Observable surface. You end up writing toSignal() adapters, managing takeUntilDestroyed(), and wiring up loading/error state manually — all boilerplate that httpResource already handles.


NSwag

NSwag takes the same class-per-tag approach, but its primary audience is .NET + ASP.NET Core shops where the spec comes from a .NET backend and the tooling integrates with MSBuild. For Angular teams without a .NET backend, the configuration overhead (an nswag.json file, NSwag Studio on Windows) adds friction without much return.

The generated output looks similar to OpenAPI Generator: @Injectable service, Observable methods, generated TypeScript interfaces. No signal support, no per-endpoint tree-shaking, no mock story beyond what you build yourself.


orval

Orval is the most actively evolving of the three established tools. It targets multiple frameworks (React Query, SWR, Axios, Angular) and has recently added an httpResource output mode — when enabled, it emits a sibling *.resource.ts file alongside the main *.service.ts.

This is a meaningful step forward, but the architecture remains additive: the Observable service is still the primary output, and httpResource is layered on top. You get both files whether you need them or not. The underlying output unit is still a service class per tag, so tree-shaking is at the class level, not the endpoint level.

Orval's strongest area is mock generation: it has solid MSW 2.x handler output, and its mock approach is well-documented. If your team is already on MSW and happy with Observable-based services, orval is a reasonable choice.


hey-api/openapi-ts

The most modern of the three, with an ESM-first approach and a "flat SDK" mode where individual functions (rather than a class) are emitted per operation. Flat mode supports tree-shaking — unused operations are not included in the bundle.

The catch is that hey-api/openapi-ts is framework-agnostic. It generates fetch/XHR clients, not Angular HttpClient calls, which means you lose Angular's interceptor chain, HttpClientTestingModule, and — most importantly — httpResource. You'd need to wrap the generated functions to integrate them with Angular's DI and signal system, at which point you're writing the glue code that the generator was supposed to eliminate.

There have also been active bugs with the Angular client output due to how it handles this references in arrow-function service files.


The fundamental difference

Every tool above generates from the same starting point: "here is an API tag, emit the methods for it." The output unit is the tag (or the whole API), and tree-shaking has to happen downstream — if at all.

openapi-resource-gen starts from a different premise: the right unit is the endpoint. One file, one token, one httpResource. esbuild's tree-shaking works at file boundaries. If you never inject(FIND_PETS_BY_STATUS), that file is never imported, and none of its code — URL string, type aliases, factory function — ships in your bundle.

The other difference is depth. The other tools stop at code generation. openapi-resource-gen ships with a mock bus for Playwright E2E, a /testing sub-entry for Vitest/Jasmine component tests, and a Chrome DevTools extension that lets you control mock state, catch in-flight requests, generate schema-valid example payloads, and share reproducible mock scenarios across your team — without touching code or reloading the page.

The philosophy: the right unit of tree-shaking is the endpoint, not the API.


Try it

npm install -D @constantant/openapi-resource-gen

npx nx g @constantant/openapi-resource-gen:api-resource \
  --specPath=https://petstore3.swagger.io/api/v3/openapi.yaml \
  --outputDir=libs/petstore-data-access/src \
  --baseUrlToken=PETSTORE_BASE_URL

Contributions are welcome — see CONTRIBUTING.md.


Built with Angular 22 · Nx · httpResource · openapi-typescript