Installation
Requirements
| Requirement | Version |
|---|---|
| Angular | 17 – 22 (>=17.0.0 <23.0.0) |
| RxJS | ^7.4.0 |
| Peer deps | @angular/core, @angular/common, rxjs — nothing else |
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
exportsmap.
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
- pnpm
- Yarn
npm install @ismailza/ngx-api-client
pnpm add @ismailza/ngx-api-client
yarn add @ismailza/ngx-api-client
Register the providers
provideApi() supplies the configuration; provideHttpClient() supplies the
interceptors. Both go in your application config.
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
]);
apiErrorInterceptormust come beforeretryInterceptor. It converts anHttpErrorResponseinto a plainApiError, andretryInterceptoronly 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.
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.
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.