Skip to main content

Making requests

ApiService is the single entry point. It is providedIn: 'root', so inject it wherever you need it — usually in a feature service that describes one resource.

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

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

Methods

MethodReturns
get<T>(endpoint, options?)Observable<T>
post<T>(endpoint, body, options?)Observable<T>
put<T>(endpoint, body, options?)Observable<T>
patch<T>(endpoint, body, options?)Observable<T>
delete<T>(endpoint, options?)Observable<T>
getPage<T>(endpoint, page, size, options?)Observable<PaginatedResponse<T>>

Every method returns the response body typed as T, not an HttpResponse. The endpoint may be written with or without a leading slash.

Requests are built lazily inside a defer(), so nothing is sent — and no loading counter moves — until you subscribe.

Query parameters

Pass params as a plain object or an HttpParams instance. Array values are appended as repeated keys.

this.api.get<Order[]>('/orders', {
params: { status: 'OPEN', tag: ['urgent', 'vip'] },
});
// → /api/v1/orders?status=OPEN&tag=urgent&tag=vip

Numbers and booleans are stringified for you.

Headers

Same idea — a plain object or an HttpHeaders instance:

this.api.post<Import>('/imports', body, {
headers: { 'Idempotency-Key': crypto.randomUUID() },
});

A header you set explicitly is never overwritten by the versioning strategy.

Pagination

getPage() appends page and size and types the result as PaginatedResponse<T>:

this.api.getPage<Order>('/orders', 0, 20);
// → /api/v1/orders?page=0&size=20
interface PaginatedResponse<T> {
content: T[];
page: {
number: number;
size: number;
totalElements: number;
totalPages: number;
};
}

Extra params are merged with the pagination pair rather than replaced:

this.api.getPage<Order>('/orders', 2, 50, { params: { status: 'OPEN' } });
// → /api/v1/orders?status=OPEN&page=2&size=50
Response shape

PaginatedResponse<T> mirrors the Spring Data REST style envelope (content plus a page object). If your backend paginates differently, use get<T>() with your own interface and pass page/size through params.

Per-request options

Every method takes an optional ApiRequestOptions as its last argument. These override the global configuration for one call only, and travel to the interceptors through the request's HttpContext — so they stay scoped to that request instead of mutating shared state.

OptionTypeEffect
versionnumber | string | falseOverride the version, or send this call unversioned
prefixstring | falseOverride the path prefix, or drop it
retryboolean | RetryConfigDisable, force, or reconfigure retry for this call
showLoaderbooleanWhether this call moves the loading indicator
skipErrorHandlerbooleanBypass the global ApiErrorHandler; the error still reaches you
successMessageboolean | stringCustom message, force one on, or suppress it
paramsHttpParams | Record<string, …>Query parameters
headersHttpHeaders | Record<string, …>Request headers
contextHttpContextYour own context tokens, for your own interceptors

Pin a call to an older version

this.api.get<LegacyOrder>('/orders', { version: 1 });

Call an endpoint outside the API surface

this.api.get<Health>('/health', { prefix: false, version: false });
// → https://api.example.com/health

Fire and forget

this.api.post('/analytics/event', payload, {
retry: false,
skipErrorHandler: true,
showLoader: false,
});

skipErrorHandler only stops the global handler from running. The ApiError is still thrown, so a subscriber can react to it.

Handle one error locally

this.api.post<Order>('/orders', draft, { skipErrorHandler: true }).subscribe({
error: (err: ApiError) => {
if (err.code === 'VALIDATION_ERROR') {
this.applyFieldErrors(err.errors ?? []);
}
},
});

Control the success message

this.api.put(`/orders/${id}`, order, { successMessage: 'Order updated' });
this.api.delete(`/orders/${id}`, { successMessage: false });

Passing your own context

If you have your own interceptors reading HttpContextTokens, pass a context and the library will add its own tokens to it rather than replacing it:

const context = new HttpContext().set(MY_FEATURE_FLAG, true);

this.api.get<Order>('/orders/1', { context, retry: false });