ngx-openapi-client
v0.3.0
Published
Type-safe, Observable-based HTTP client for Angular, powered by openapi-typescript-generated types
Maintainers
Readme
ngx-openapi-client
A type-safe, Observable-based HTTP client for Angular, powered by your OpenAPI spec.
ngx-openapi-client wraps Angular's HttpClient with types generated by
openapi-typescript. Paths, path/query parameters, request
bodies, and response bodies are all checked at compile time against your API spec —
with zero runtime overhead beyond a thin request wrapper, and full participation in
Angular's DI and interceptor pipeline.
const client = createClient<paths>({ baseUrl: 'https://api.example.com' });
client.GET('/pets/{id}', { params: { path: { id: 42 } } });
// ✅ URL, params, and response typed from your OpenAPI spec
// ❌ client.GET('/petz') — compile error: unknown path
// ❌ path: { id: 'not-a-number' } — compile error: id must be numberDesign goals
This library is deliberately small. It exists to make one workflow safe and effortless — and to encourage you to keep it that way:
- The spec is the single source of truth. API types are generated from your OpenAPI document, never written by hand. Regenerate on every install and when the API changes, the compiler — not a runtime error in production — points at every call site that broke.
- Zero footprint. The entire type layer erases at compile time. The only code
you ship is a thin delegation to Angular's
HttpClient— no parallel HTTP stack, no response transformation, no hidden state. What goes over the wire is exactly whatHttpClientwould send. - Angular-native. Requests run through your existing DI, interceptors, and
HttpContext; tests keep usingHttpTestingController. Nothing to configure twice. - Signals over subscriptions. Every method returns an Observable so it
composes with the ecosystem — but don't
.subscribe()yourself. Feed it to the resource API (rxResource) and consumevalue()/isLoading()/error()declaratively; Angular manages the subscription lifecycle for you.
Installation
npm install ngx-openapi-client
npm install --save-dev openapi-typescriptRequires Angular ≥ 21.2 and rxjs ≥ 7.4 (peer dependencies).
Support policy: the library supports every Angular major that is still within its official support window (currently 21 and 22). Support for a major is dropped — as a semver-major release of this library — once it leaves Angular LTS.
Generate types from your spec
npx openapi-typescript ./openapi/api.yaml -o ./src/app/api/schema.tsTip: wire it to your prepare script so types regenerate on every install.
Usage
createClient resolves HttpClient via inject(), so call it in an injection
context — a field initializer of a service is the natural place:
import { Injectable } from '@angular/core';
import type { HttpResponse } from '@angular/common/http';
import type { Observable } from 'rxjs';
import { createClient } from 'ngx-openapi-client';
import type { SchemaOf } from 'ngx-openapi-client';
import type { components, paths } from './api/schema';
type Pet = SchemaOf<components, 'Pet'>;
@Injectable({ providedIn: 'root' })
export class PetsApi {
private readonly client = createClient<paths>({
baseUrl: 'https://api.example.com',
headers: { 'X-Api-Version': '2' }, // sent with every request
});
listPets(search?: string): Observable<HttpResponse<Pet[]>> {
return this.client.GET('/pets', { params: { query: { search } } });
}
createPet(name: string): Observable<HttpResponse<Pet>> {
return this.client.POST('/pets', { body: { name } });
}
deletePet(id: number): Observable<HttpResponse<never>> { // 204 — no body
return this.client.DELETE('/pets/{id}', { params: { path: { id } } });
}
}All seven methods are available: GET, POST, PUT, PATCH, DELETE, HEAD,
OPTIONS. Each accepts only the paths that declare that method in your spec.
Responses and errors
Every method returns a cold Observable of Angular's raw
HttpResponse<SuccessBody> — status, headers, and the typed body are all there,
and nothing is hidden behind a wrapper. Consume it as a signal with Angular's
resource API instead of subscribing manually:
import { Component, inject, signal } from '@angular/core';
import { rxResource } from '@angular/core/rxjs-interop';
@Component({ /* ... */ })
export class PetListComponent {
private readonly petsApi = inject(PetsApi);
readonly search = signal('');
readonly pets = rxResource({
params: () => ({ search: this.search() }),
stream: ({ params }) => this.petsApi.listPets(params.search),
});
}@if (pets.value(); as response) {
<ul>
@for (pet of response.body; track pet.id) {
<li>{{ pet.name }}</li>
}
</ul>
} @else if (pets.isLoading()) {
<p>Loading…</p>
}Updating the search signal re-triggers the request automatically; pets.value()
is the typed HttpResponse<Pet[]>.
Non-2xx responses follow Angular convention and arrive on the error channel as
HttpErrorResponse — with rxResource that surfaces as pets.error().
Interceptors, retry, catchError, and everything else in your existing HTTP
setup work unchanged.
Typed error bodies
HttpErrorResponse.error is any, so by itself the error body loses all contract
information. createErrorGuard restores it without a single cast: build a
guard for one operation from its ErrorResponses map, and both the accepted
status codes and the narrowed body types come straight from your spec.
import { computed } from '@angular/core';
import { createErrorGuard } from 'ngx-openapi-client';
import type { ErrorResponses } from 'ngx-openapi-client';
const isListPetsError = createErrorGuard<ErrorResponses<paths, '/pets', 'get'>>();
// inside PetListComponent
readonly errorMessage = computed(() => {
const err = this.pets.error();
if (isListPetsError(err, 400)) {
return err.error.message; // ApiError — fully typed, no cast
}
return err ? 'Something went wrong.' : undefined;
});The code argument autocompletes to exactly the response keys your spec declares
for that operation: numeric status codes, range keys like '5XX', and
'default'. Two things to know:
- Declared codes are erased at runtime, so a range or
'default'check cannot exclude codes the spec also declares explicitly. Order your branches specific → range →'default'— the same precedence OpenAPI itself defines. 'default'never matches network-level failures (status 0, e.g. timeout or offline); those carry aProgressEventinstead of a contract body.
Per-request options
this.client.GET('/pets/{id}', {
params: { path: { id }, query: { verbose: true } },
headers: { 'X-Request-Id': requestId }, // merged over client headers; request wins
context: new HttpContext(), // passed to Angular interceptors
});Security
- Path parameters are percent-encoded per segment;
.is additionally encoded as%2E, so a user-supplied value like../admincannot be normalised into a path traversal by the server. - Query parameters:
&,#, and%in values stay percent-encoded — a value cannot inject additional parameters or truncate the query string. - Headers are always passed to
HttpClientas a structured record, never as a raw string — a CRLF in a header value cannot inject an additional header.
Each of these invariants is pinned by a regression test.
Scope and limitations
- Path parameters: OpenAPI style
simple(the default) with scalar values.label/matrixstyles and array/object values are not supported. - Query parameters: scalar values. Arrays,
spaceDelimited,pipeDelimited, anddeepObjectstyles are not supported. Note that Angular's default codec sends a literal+in values (servers decode it as a space). - Cookie parameters (
in: cookie) are out of scope. - Request/response bodies:
application/jsononly.
Exported types
Besides createClient and createErrorGuard, the package exports the type
utilities it is built from, usable in your own signatures: ClientOptions,
RequestInit, NgOpenApiClient, HttpMethod, PathsWithMethod,
RequestParams, RequestBody, SuccessResponseBody, ErrorResponseBody,
ErrorResponses, TypedHttpErrorResponse, SchemaOf, SchemaName.
License
MIT
