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

@ismailza/ngx-api-client

v0.3.1

Published

A typed, interceptor-driven HTTP layer for Angular: versioned URLs, RFC 9457 problem+json error normalisation, retry with backoff, and pluggable error/success handlers.

Readme

HttpClient gives you a request. It doesn't give you a policy — where the base URL comes from, how a failed response becomes something your components can render, which requests are safe to retry, or how a loading indicator knows anything is in flight. Most apps end up re-solving all four in an ad-hoc ApiService. This is that service, extracted and made configurable.

  • Configurable versioning — URL segment, query parameter, header or media type; or turned off entirely — overridable per request
  • RFC 9457 problem+json errors normalised into one ApiError shape, whatever the backend returns
  • Retry with exponential backoff, jitter and Retry-After, skipping non-idempotent methods
  • Per-request options carried to interceptors through HttpContext, not globals
  • Pluggable presentation — the library never renders anything; you decide what an error looks like
  • Loading signal for a global progress bar
  • No UI dependency. Peers are @angular/core, @angular/common and rxjs. That's it.

Install

npm install @ismailza/ngx-api-client

Setup

import { provideHttpClient, withInterceptors } from '@angular/common/http';
import {
  provideApi,
  apiErrorInterceptor,
  apiSuccessInterceptor,
  retryInterceptor,
} from '@ismailza/ngx-api-client';

export const appConfig: ApplicationConfig = {
  providers: [
    provideApi({
      baseUrl: 'https://api.example.com',
      prefix: 'api', // {baseUrl}/api/...  — '' or false to omit
      version: 1,
      versioning: 'url', // 'query-param' | 'header' | 'media-type' | false
      retry: { maxRetries: 3, initialDelay: 1000 },
    }),
    provideHttpClient(
      withInterceptors([apiErrorInterceptor, apiSuccessInterceptor, retryInterceptor]),
    ),
  ],
};

Interceptors are registered by you, not by provideApi(), because order matters and only you know what else is in the chain. The first interceptor in the array is the outermost one:

  • Keep apiErrorInterceptor before retryInterceptor. It replaces the HttpErrorResponse with a plain ApiError, and retryInterceptor only retries HttpErrorResponses — nest them the other way round and retry silently never fires. Outermost, it also reports one failure per operation rather than one per attempt.
  • Put an auth/bearer-token interceptor last, inside retryInterceptor, so each retried attempt is signed with a fresh token.

Usage

@Injectable({ providedIn: 'root' })
export class OrderService {
  private readonly api = inject(ApiService);

  list(page: number, size: number): Observable<PaginatedResponse<Order>> {
    return this.api.getPage<Order>('/orders', page, size);
  }

  get(id: string): Observable<Order> {
    return this.api.get<Order>(`/orders/${id}`);
  }

  create(order: OrderRequest): Observable<Order> {
    return this.api.post<Order>('/orders', order);
  }
}

GET /orders with version: 1 resolves to https://api.example.com/api/v1/orders?page=0&size=20.

URL shape and versioning

A URL is {baseUrl} + an optional {prefix} segment + an optional version segment + {endpoint}. Both middle pieces are yours to configure — or remove.

provideApi({ baseUrl: 'https://api.example.com', prefix: 'api', version: 2 });
// → https://api.example.com/api/v2/orders

prefix is a static path segment (default 'api'); set it to '' or false when the API is served from the root. versioning picks how the version travels:

| versioning | Result for GET /orders | | --------------------------------------------------- | ------------------------------------- | | 'url' (default) | /api/v2/orders | | { strategy: 'url', prefix: '' } | /api/2/orders | | 'query-param' | /api/orders?v=2 | | { strategy: 'query-param', parameterName: 'ver' } | /api/orders?ver=2 | | 'header' | /api/orders + X-API-Version: 2 | | { strategy: 'header', headerName: 'Api-Version' } | /api/orders + Api-Version: 2 | | 'media-type' | Accept: application/vnd.api.v2+json | | false | /api/orders, no version sent |

For 'media-type', mediaType is a template whose {version} placeholders are substituted — default 'application/vnd.api.v{version}+json'.

version accepts strings too, for date- or label-based schemes:

provideApi({
  baseUrl: 'https://api.example.com',
  version: '2024-01-01',
  versioning: { strategy: 'header', headerName: 'Api-Version' },
});

A header or query parameter the caller sets explicitly is never overwritten by the versioning strategy, and a version placed in the URL is percent-encoded. With versioning: false there is no strategy to carry a version, so a per-request version is ignored.

Per-request options

Every method takes an optional ApiRequestOptions:

// Pin this call to an older API version
this.api.get<LegacyOrder>('/orders', { version: 1 });

// Hit an unversioned endpoint outside the API prefix: /health
this.api.get<Health>('/health', { prefix: false, version: false });

// Fire-and-forget: no retry, no global error handler, no loading indicator
this.api.post('/analytics/event', payload, {
  retry: false,
  skipErrorHandler: true,
  showLoader: false,
});

// Custom success message; or none at all
this.api.put(`/orders/${id}`, order, { successMessage: 'Order updated' });
this.api.delete(`/orders/${id}`, { successMessage: false });

These travel to the interceptors via HttpContext, so they stay scoped to the one request instead of mutating shared state.

Error handling

Every failure — a problem+json body, a plain-text proxy error page, or a network drop that never reached the server — arrives as the same ApiError:

interface ApiError {
  type: string; // RFC 9457 problem type URI
  title?: string; // 'Bad Request'
  status: number; // 0 for a network failure
  detail: string; // safe to show the user
  instance?: string; // path that produced it
  code?: string; // machine-readable, e.g. 'VALIDATION_ERROR'
  timestamp: string;
  traceId?: string; // body, else the X-Trace-Id header
  errors?: { field: string; message: string }[];
}

Branch on code, never on detail text.

The library will not render this for you. provideApi() registers a fallback that forwards to Angular's ErrorHandler and nothing more. To show something, provide your own:

@Injectable()
export class ToastApiErrorHandler extends ApiErrorHandler {
  private readonly toast = inject(MyToastService);
  private readonly router = inject(Router);

  override handle(error: ApiError): void {
    this.toast.error(error.detail, { title: error.title });

    if (error.status === 403) {
      this.router.navigate(['/forbidden']);
    }
  }
}

// in appConfig.providers
{ provide: ApiErrorHandler, useClass: ToastApiErrorHandler }

Note: the fallback hands the whole ApiError to ErrorHandler, which logs it. If your API puts sensitive data in detail or instance, register your own handler rather than relying on the default.

ApiSuccessHandler works the same way and defaults to discarding the message.

Retry

Applied only to responses whose status is in retryableStatuses (408, 429, 500, 502, 503, 504 by default):

| Behaviour | Detail | | ------------- | ---------------------------------------------------------------------- | | Backoff | initialDelay * multiplier^(attempt-1), plus 0–30% jitter | | Retry-After | Honoured on 429, in seconds or as an HTTP date — overrides the backoff | | POST | Not retried unless the request opts in, since it isn't idempotent | | Opt out | retry: false per request, or globally in provideApi() |

Loading state

ApiLoadingService counts in-flight requests and exposes a signal, so concurrent calls don't flicker the indicator off early:

@Component({
  template: `@if (loading.loading()) {
    <my-progress-bar />
  }`,
})
export class AppShell {
  protected readonly loading = inject(ApiLoadingService);
}

Exclude a request with showLoader: false.

Configuration

| Option | Default | Description | | --------------------------- | ------------------------------------------------------ | --------------------------------------------------- | | baseUrl | required | API root; a trailing slash is trimmed | | prefix | 'api' | Static path segment; ''/false omits it | | version | 1 | Version value, number or string | | versioning | 'url' | Strategy name, config object, or false to disable | | retry | { maxRetries: 3, initialDelay: 1000, multiplier: 2 } | false disables retry globally | | defaultShowLoader | true | Whether requests track loading state | | defaultShowSuccessMessage | true | Success message on mutating methods | | defaultSuccessMessages | per-method English defaults | Merged over the built-ins |

Every option falls back to its default when omitted or when passed explicitly as undefined, so building the config from optional sources (prefix: environment.apiPrefix) is safe.

Success messages are plain strings so the package carries no i18n dependency — pass translated values into defaultSuccessMessages, or handle wording in your own ApiSuccessHandler.

Compatibility

| Requirement | Supported | | ----------------- | --------------------------------------------------------- | | Angular | 17 · 18 · 19 · 20 · 21 · 22 | | RxJS | ^7.4.0 | | Peer dependencies | @angular/core, @angular/common, rxjs — nothing else |

Every Angular major above is verified on each push, against the packed tarball rather than the source. CI installs the package into a throwaway consumer application per version and runs three checks: a type-check against that version's Angular types, a production ng build — which is what exercises the Angular linker over the shipped partial declarations — and a runtime test of the injector and the exports map. Each combination runs on both Node 22 and 24.

The peer range is bounded (>=17.0.0 <23.0.0) deliberately: a new Angular major is added only once it has been validated, so the supported range is a tested claim rather than an assumption.

Contributing

Contributions are welcome! Please read our Contributing Guide before opening an issue or pull request.

Code of Conduct

Please read our Code of Conduct to help us maintain a welcoming and inclusive community.

License

MIT © Ismail ZAHIR

Support the Project

If you find this library useful, consider giving it a ⭐ on GitHub. It helps others discover the project and motivates future development.