http-egy
v0.4.7
Published
An Angular HTTP client extension built on top of `HttpClient`. Removes repetitive boilerplate while keeping full compatibility with Angular's native API.
Readme
HttpEgy
An Angular HTTP client extension built on top of HttpClient. Removes repetitive boilerplate while keeping full compatibility with Angular's native API.
Features
- Clean request configuration with plain objects
- Plain object headers and query params — no
HttpParams/HttpHeadersceremony - Global base URL and default headers
- Signal-based reactive resources (
watch,watchGet,watchMutation) - Function-based interceptors with sync and async support
- RxJS operator support in reactive pipelines
- Tree-shakable and fully standalone
- Typed responses and errors
- Timeout and
AbortSignalsupport - Zero NgModules — pure functional providers
Why HttpEgy?
Angular's HttpClient is intentionally low-level. It gives you full control but requires repetitive setup for common tasks:
- Building
HttpParamsandHttpHeadersobjects - Configuring timeouts and base URLs per request
- Writing boilerplate for loading and error states
- Manually connecting reactive signals to HTTP calls
HttpEgy builds on top of HttpClient by providing:
- Config-object-based methods — headers and query params are plain objects; the library converts them internally
- Reactive resources — signal-based handles that track
value,isLoading,errorautomatically - Global configuration — set base URL, default headers, default options, and interceptors once
- Function-based interceptors — simpler than Angular's handler-chain pattern
Every method returns a standard RxJS Observable, so you can use pipe(), toSignal(), toObservable(), or any RxJS operator as usual.
Installation
npm install http-egyConfiguration
Call provideHttp():
import { provideHttp } from 'http-egy';
bootstrapApplication(App, {
providers: [
provideHttp({
baseUrl: 'https://api.example.com',
defaultHeaders: {
'Content-Type': 'application/json',
},
defaultOptions: {
withCredentials: true,
timeout: 10_000,
},
}),
],
});| Option | Type | Default | Description |
|---|---|---|---|
| baseUrl | string | '' | Prepended to all request URLs |
| defaultHeaders | Record<string, string \| string[]> | {} | Default headers for every request |
| defaultOptions | RequestOptions | {} | Default timeout, withCredentials, etc. |
| interceptors | HttpInterceptorFn[] | [] | Library interceptor functions |
| angularInterceptors | Angular HttpInterceptorFn[] | [] | Angular HttpClient interceptors |
Basic Usage
injectHttp
import { injectHttp } from 'http-egy';
@Component({...})
export class UsersComponent {
private http = injectHttp();
}Basic requests
this.http.get<User[]>('/api/users');
this.http.post<User>('/api/users', { body: { name: 'John' } });
this.http.put<User>('/api/users/1', { body: { name: 'John Updated' } });
this.http.patch<User>('/api/users/1', { body: { name: 'John Patched' } });
this.http.delete<void>('/api/users/1');Query parameters
this.http.get<User[]>('/api/users', {
query: {
page: 1,
limit: 20,
roles: ['admin', 'editor'],
},
});Headers
this.http.get<User>('/api/users/me', {
headers: {
Authorization: `Bearer ${token}`,
'X-Request-ID': crypto.randomUUID(),
},
});Response types
// Observe body (default)
this.http.get<User[]>('/api/users');
// Observe full response
this.http.get('/api/users', {
options: { observe: 'response' },
});
// Observe events (progress)
this.http.get('/api/large-file', {
options: { observe: 'events', reportProgress: true, responseType: 'blob' },
});Timeout
this.http.get('/api/users', {
options: { timeout: 5_000 },
});Cancellation
const controller = new AbortController();
this.http.get('/api/users', {
options: { signal: controller.signal },
});
controller.abort();Reactive Resources
Reactive resources return an HttpResource<T, E> — a signal-based handle that tracks the full lifecycle of a request:
value— last successful response body (Signal<T | undefined>)isLoading— true while request is in flight (Signal<boolean>)error— last error, if any (Signal<E | undefined>)hasValue()— true ifvalue()is notundefinedreload(data?)— re-run the request
watch() — reactive reads
Re-runs the request automatically whenever any signal read inside source() changes.
const postId = signal(1);
const post = this.http.watch<Post>({
source: () => ({
method: 'GET',
url: `/posts/${postId()}`,
}),
});
// post.value() — Signal<T | undefined>
// post.isLoading() — Signal<boolean>
// post.error() — Signal<HttpErrorResponse | undefined>watchGet() — reactive read shorthand
const posts = this.http.watchGet<Post[]>({
source: () => `/posts?page=${page()}`,
});watchMutation() — explicit-trigger only
Never tracks signals. Changing a signal inside source() will not start a request. The request executes only when reload() is called.
readonly loginResult = this.http.watchMutation<LoginResponse>({
source: () => ({
method: 'POST',
url: '/api/login',
body: this.loginForm.value,
}),
});
login() {
this.loginResult.reload();
}reload(data?)
Forwards data to the source() function so it can adjust the request config:
readonly users = this.http.watch<User[]>({
source: (data?: { page?: number }) => ({
method: 'GET',
url: '/api/users',
query: { page: data?.page ?? 1 },
}),
});
this.users.reload({ page: 2 });
this.users.reload(); // source receives undefinedRxJS operators
Both watch() and watchMutation() accept an operators function:
const users = this.http.watch<User[]>({
source: () => ({ method: 'GET', url: '/api/users' }),
operators: (obs$) => obs$.pipe(
tap(list => console.log('Loaded:', list.length)),
map(list => list.filter(u => u.active)),
),
});Interceptors
Library interceptors are functions that receive a RequestConfig and return a modified one. They can be synchronous or asynchronous.
import type { HttpInterceptorFn } from 'http-egy';
// Synchronous
const loggingInterceptor: HttpInterceptorFn = (config) => {
console.log(`[HTTP] ${config.method} ${config.url}`);
return config;
};
// Async (Observable)
const tokenInterceptor: HttpInterceptorFn = (config) =>
authService.refreshToken().pipe(
map(token => ({
...config,
headers: { ...config.headers, Authorization: `Bearer ${token}` },
})),
);
// Async (Promise)
const promiseInterceptor: HttpInterceptorFn = async (config) => {
const token = await authService.getToken();
return { ...config, headers: { ...config.headers, Authorization: `Bearer ${token}` } };
};Registration:
provideHttp({ interceptors: [loggingInterceptor, tokenInterceptor] })Angular interceptors can also be used alongside library interceptors:
provideHttp({
angularInterceptors: [myAngularInterceptor],
})Error Handling
The error type E defaults to HttpErrorResponse. Override it when your interceptor normalizes errors:
interface ApiError { message: string; code: string; }
const login = this.http.watchMutation<LoginResponse, ApiError>({
source: () => ({ method: 'POST', url: '/api/login', body: this.loginForm.value }),
});
if (login.error()) {
console.log(login.error()?.message); // typed as ApiError
}Without custom types, access the standard HttpErrorResponse fields:
this.http.get<User[]>('/api/users').pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Redirect to login
}
return throwError(() => error);
}),
);Why not HttpClient directly?
| Concern | HttpClient | HttpEgy |
|---|---|---|
| Headers | new HttpHeaders({...}) | Plain object |
| Query params | new HttpParams({...}) | Plain object |
| Base URL | Manual concatenation | baseUrl in config |
| Default headers | Manual per request | defaultHeaders in config |
| Timeout | Manual AbortSignal | timeout option |
| Interceptors | Class-based or complex chain | Function-based |
| Reactive resources | Manual toSignal + switchMap | Built-in watch() / watchMutation() |
| Loading/error state | Manual signals | Built-in isLoading / error |
| Boilerplate | High (params, headers, options per call) | Low (config object) |
HttpEgy builds on top of HttpClient without replacing it. Every method returns a standard RxJS Observable, and you can still use Angular interceptors alongside library interceptors.
Architecture
RequestConfig (method, url, headers, query, body, options)
│
▼
Library interceptors (modify config — sync or async)
│
▼
Defaults merge (baseUrl, defaultHeaders, defaultOptions)
│
▼
Headers & query conversion (plain objects → HttpHeaders / HttpParams)
│
▼
Angular HttpClient (request execution)
│
▼
Response mapping (body / response / events)
│
▼
Observable<T> / HttpResource<T, E>API Reference
provideHttp(config?)
| Option | Type | Default | Description |
|---|---|---|---|
| baseUrl | string | '' | Prepended to all request URLs |
| defaultHeaders | Record<string, string \| string[]> | {} | Default headers for every request |
| defaultOptions | RequestOptions | {} | Default request options |
| interceptors | HttpInterceptorFn[] | [] | Library interceptor functions |
| angularInterceptors | Angular HttpInterceptorFn[] | [] | Angular HttpClient interceptors |
injectHttp()
Returns the singleton HttpService instance. Must be called within an injection context.
HttpService
| Method | Returns | Description |
|---|---|---|
| request<T>(config) | Observable<T> | Full request with RequestConfig |
| get<T>(url, config?) | Observable<T> | GET request |
| post<T>(url, config?) | Observable<T> | POST request |
| put<T>(url, config?) | Observable<T> | PUT request |
| patch<T>(url, config?) | Observable<T> | PATCH request |
| delete<T>(url, config?) | Observable<T> | DELETE request |
| watch<T, E>(config) | HttpResource<T, E> | Reactive read — tracks signals |
| watchGet<T, E>(config) | HttpResource<T, E> | Reactive read shorthand (GET) |
| watchMutation<T, E>(config) | HttpResource<T, E> | Explicit-trigger mutation |
HttpResource<T, E>
| Member | Type | Description |
|---|---|---|
| value | Signal<T \| undefined> | Last successful response body |
| isLoading | Signal<boolean> | True while a request is in flight |
| error | Signal<E \| undefined> | Last error (default HttpErrorResponse) |
| hasValue() | () => boolean | True if value() is not undefined |
| reload(data?) | (data?: unknown) => void | Re-runs the request; data forwarded to source |
RequestConfig
| Field | Type | Description |
|---|---|---|
| method | 'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' | HTTP method |
| url | string | Request URL (relative or absolute) |
| headers | Record<string, string> | HTTP headers |
| query | Record<string, string \| string[]> | Query parameters |
| body | unknown | Request body |
| options | RequestOptions | Angular HttpClient options |
RequestOptions
| Option | Type | Default | Description |
|---|---|---|---|
| observe | 'body' \| 'response' \| 'events' | 'body' | What the Observable emits |
| responseType | 'json' \| 'text' \| 'blob' \| 'arraybuffer' | 'json' | Expected response format |
| reportProgress | boolean | false | Emit progress events |
| withCredentials | boolean | false | Send cookies with cross-origin requests |
| timeout | number | 0 | Max time in ms before abort |
| signal | AbortSignal | — | Manual cancellation signal |
HttpResponse<T>
Returned when observe: 'response' is used.
| Field | Type | Description |
|---|---|---|
| data | T | Response body |
| status | number | HTTP status code |
| statusText | string | HTTP status text |
| headers | Record<string, string> | Response headers |
| url | string | Response URL |
License
MIT
