Skip to main content

Installation

Requirements

RequirementVersion
Angular17 – 22 (>=17.0.0 <23.0.0)
RxJS^7.4.0
Peer deps@angular/core, @angular/common, rxjs — nothing else
Every version in that range is tested

Compatibility is verified on each push, against the packed tarball rather than the source. For each Angular major, CI installs the package into a throwaway consumer application 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 partial declarations the package ships;
  • a runtime test of the injector and the exports map.

Each combination runs on both Node 22 and 24. The upper bound is deliberate — a new Angular major is added only once it has been validated.

Install

npm install @ismailza/ngx-api-client

Register the providers

provideApi() supplies the configuration; provideHttpClient() supplies the interceptors. Both go in your application config.

src/app/app.config.ts
import { ApplicationConfig } from '@angular/core';
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',
}),
provideHttpClient(
withInterceptors([apiErrorInterceptor, apiSuccessInterceptor, retryInterceptor]),
),
],
};

baseUrl is the only required option. Everything else has a default — see Configuration.

Interceptor order matters

Interceptors are registered by you, not by provideApi(), because the order determines the behaviour and only you know what else is in the chain.

The rule to remember: the first interceptor in the array is the outermost one. Each subsequent interceptor is nested inside it, and the last one sits closest to the network.

withInterceptors([
apiErrorInterceptor, // 1. outermost — normalises the failure that survived every retry
apiSuccessInterceptor, // 2. reports the messages of successful calls
retryInterceptor, // 3. retries transient failures
bearerTokenInterceptor, // 4. innermost — re-signs every single attempt
]);
  • apiErrorInterceptor must come before retryInterceptor. It converts an HttpErrorResponse into a plain ApiError, and retryInterceptor only recognises the former. Nest the error interceptor inside the retry one and retry silently stops working — every request gets exactly one attempt. Outermost, it also fires your handler once per failed operation rather than once per attempt.
  • Put an auth/bearer-token interceptor last, inside retryInterceptor, so each retried attempt is re-signed with a fresh token instead of replaying an expired one.
A common mistake

withInterceptors([retryInterceptor, apiErrorInterceptor]) looks like the natural reading order, but it disables retry entirely. Errors reach retryInterceptor already normalised, and it has nothing left to match on.

Register only what you use

The three interceptors are independent. If you don't want automatic retry, leave retryInterceptor out — the rest keeps working. ApiService itself works with no interceptors at all; you just lose error normalisation, retry and success messages.

Verify the setup

Inject ApiService anywhere and call an endpoint:

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

check(): Observable<{ status: string }> {
return this.api.get('/health', { prefix: false, version: false });
}
}

With the config above, that requests https://api.example.com/health.

Next

Head to the Quick start to build a complete, typed data service.