@almvasiliev/http-client
v1.0.0
Published
Lightweight, production-ready HTTP client for Node.js ≥18 built on native Fetch (Undici). Supports streaming, interceptors, retries, pluggable auth, and seamless NestJS integration — fully typed with zero runtime dependencies.
Downloads
26
Maintainers
Readme
@almvasiliev/http-client
Lightweight, production-ready HTTP client for Node.js ≥18 built on native Fetch (Undici). Supports streaming, interceptors, retries, pluggable auth, and seamless NestJS integration — fully typed with zero runtime dependencies.
Installation
npm install @almvasiliev/http-clientFeatures
| Feature | Description |
|---|---|
| Native fetch | Uses built-in fetch (undici) |
| Streaming | ReadableStream<Uint8Array> for OpenAI-compatible SSE interfaces |
| Timeout | Configurable HTTP timeout (default 30 000 ms) |
| Retry | Exponential backoff with configurable conditions and statuses |
| Interceptors | Pipeline onRequest → onResponse → onError |
| Logging | Independent control: all requests (default OFF), errors (default ON) |
| Sanitize headers | Masking sensitive headers in logs (default list is empty) |
| Auth providers | BasicAuth, BearerAuth and extensible HttpAuthProvider interface |
| Custom logger | Pass your own logger via the HttpLogger interface |
| Dual entry | Framework-agnostic (./) and NestJS (./nestjs) supports |
Configuration
All parameters are passed when initializing the client (HttpClient) or the module (HttpModule.forRoot() / forRootAsync()).
Full parameter list
| Parameter | Type | Default | Description |
|---|---|---|---|
| baseUrl | string | — | Base URL for all requests |
| timeout | number | 30000 | HTTP timeout in milliseconds |
| defaultHeaders | Record<string, string> | — | Default headers for all requests |
| logging | boolean \| HttpLoggingOptions | { all: false, errors: true } | Logging configuration (see below) |
| logger | HttpLogger | Console | Custom logger (native HttpClient only) |
| retry | HttpRetryOptions | see below | Retry configuration (see below) |
Retry (retry)
| Parameter | Type | Default | Description |
|---|---|---|---|
| maxRetries | number | 0 | Maximum number of retry attempts |
| retryDelay | number | 1000 | Base delay between attempts (ms) |
| retryableStatuses | number[] | [408, 429, 500, 502, 503, 504] | HTTP statuses that trigger a retry |
| exponentialBackoff | boolean | false | Exponential delay growth |
| retryCondition | (error) => boolean | — | Custom retry condition |
| onRetry | (count, error) => void | — | Callback on each retry |
Logging (logging)
| Parameter | Type | Default | Description |
|---|---|---|---|
| all | boolean | false | Log all requests and responses |
| errors | boolean | true | Log errors |
| sanitizeHeaders | string[] | [] | List of headers to mask in logs (case-insensitive) |
Shortcuts:
logging: true— enable everything ({ all: true, errors: true })logging: false— disable everything ({ all: false, errors: false })
Authorization (Auth Providers)
The package provides an extensible authorization system based on the HttpAuthProvider interface.
Built-in providers
| Provider | Description |
|---|---|
| BasicAuth | HTTP Basic Authentication (Basic base64(username:password)) |
| BearerAuth | Bearer token (Bearer <token>) |
| AuthInterceptor | Interceptor for automatic injection of the Authorization header |
Native usage
import { HttpClient } from '@almvasiliev/http-client';
const client = new HttpClient({
baseUrl: 'https://api.example.com',
timeout: 5000,
logging: {
all: true,
errors: true,
sanitizeHeaders: ['authorization', 'x-api-key'],
},
retry: {
maxRetries: 3,
retryDelay: 1000,
retryableStatuses: [408, 429, 500, 502, 503, 504],
exponentialBackoff: true,
},
});
// GET
const { data } = await client.get<User[]>('/users');
// POST
const { data: created } = await client.post<User>('/users', { name: 'John' });
// Streaming (OpenAI SSE)
const { stream, status } = await client.stream({
url: '/v1/chat/completions',
method: 'POST',
data: { model: 'gpt-4', messages: [...], stream: true },
});
const reader = stream!.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
console.log(decoder.decode(value));
}Timeout
// Global timeout of 10 seconds
const client = new HttpClient({ timeout: 10_000 });
// Override at request level
await client.get('/slow-endpoint', { timeout: 60_000 });
// No timeout (for streaming — default timeout: 0)
const { stream } = await client.stream({ url: '/v1/completions', method: 'POST', data: body });Retry
// No retry (default)
const client = new HttpClient({ baseUrl: 'https://api.example.com' });
// With retry: 3 attempts, exponential backoff
const client = new HttpClient({
baseUrl: 'https://api.example.com',
retry: {
maxRetries: 3,
retryDelay: 1000,
exponentialBackoff: true,
},
});
// Custom retryable statuses
const client = new HttpClient({
retry: {
maxRetries: 2,
retryableStatuses: [429, 503],
},
});
// Custom retry condition
const client = new HttpClient({
retry: {
maxRetries: 5,
retryCondition: (error) => error.status === 429,
onRetry: (count, error) => console.log(`Retry #${count}, status: ${error.status}`),
},
});
// Override retry at request level
await client.get('/unstable', {
retry: { maxRetries: 5, retryDelay: 2000 },
});Logging: sanitizeHeaders
// Masking sensitive headers in logs
const client = new HttpClient({
logging: {
all: true,
errors: true,
sanitizeHeaders: ['authorization', 'x-api-key', 'cookie', 'set-cookie'],
},
});
// In logs: headers: { authorization: '[REDACTED]', 'content-type': 'application/json' }Logging: control
// Log everything (requests + responses + errors)
new HttpClient({ logging: true });
// Log errors only (default)
new HttpClient({ logging: { all: false, errors: true } });
// Everything disabled
new HttpClient({ logging: false });
// Requests/responses only, no errors
new HttpClient({ logging: { all: true, errors: false } });Custom logger
import { HttpClient, type HttpLogger } from '@almvasiliev/http-client';
const myLogger: HttpLogger = {
log: (message, data) => console.info(`[HTTP] ${message}`, data),
error: (message, data) => console.error(`[HTTP ERROR] ${message}`, data),
};
const client = new HttpClient({
baseUrl: 'https://api.example.com',
logging: {
all: true,
errors: true,
sanitizeHeaders: ['authorization'],
},
logger: myLogger,
});Authorization BasicAuth
import { HttpClient, BasicAuth, AuthInterceptor } from '@almvasiliev/http-client';
const auth = new BasicAuth({ username: 'admin', password: 'secret' });
// Option 1: via defaultHeaders
const client = new HttpClient({
baseUrl: 'https://api.example.com',
defaultHeaders: {
Authorization: auth.getAuthHeader(),
},
});
// Option 2: via AuthInterceptor (recommended)
const client2 = new HttpClient({ baseUrl: 'https://api.example.com' });
client2.addInterceptor(new AuthInterceptor(auth));
// Option 3: per-request
const client3 = new HttpClient({ baseUrl: 'https://api.example.com' });
await client3.get('/protected', {
headers: { Authorization: auth.getAuthHeader() },
});Authorization BearerAuth
import { HttpClient, BearerAuth, AuthInterceptor } from '@almvasiliev/http-client';
const auth = new BearerAuth('my-jwt-token');
const client = new HttpClient({ baseUrl: 'https://api.example.com' });
client.addInterceptor(new AuthInterceptor(auth));
await client.get('/protected'); // Authorization: Bearer my-jwt-tokenNestJS usage
import { HttpModule, HttpService } from '@almvasiliev/http-client/nestjs';
// app.module.ts — full configuration
@Module({
imports: [
HttpModule.forRoot({
baseUrl: 'https://api.example.com',
timeout: 5000,
logging: {
all: false,
errors: true,
sanitizeHeaders: ['authorization', 'x-api-key'],
},
retry: {
maxRetries: 3,
retryDelay: 1000,
retryableStatuses: [408, 429, 500, 502, 503, 504],
exponentialBackoff: true,
},
}),
],
})
export class AppModule {}Async configuration
HttpModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
baseUrl: config.get('API_URL'),
timeout: Number(config.get('HTTP_TIMEOUT', 30000)),
logging: {
all: config.get('HTTP_LOG_ALL') === 'true',
errors: true,
sanitizeHeaders: ['authorization', 'x-api-key', 'cookie'],
},
retry: {
maxRetries: Number(config.get('HTTP_MAX_RETRIES', 0)),
retryDelay: Number(config.get('HTTP_RETRY_DELAY', 1000)),
retryableStatuses: [408, 429, 500, 502, 503, 504],
exponentialBackoff: true,
},
}),
});Service usage
@Injectable()
export class UserService {
constructor(private readonly httpService: HttpService) {}
async getUsers() {
return this.httpService.get<User[]>('/users');
}
async streamCompletion(messages: Message[]) {
const { stream } = await this.httpService.stream({
url: '/v1/chat/completions',
method: 'POST',
data: { model: 'gpt-4', messages, stream: true },
});
return stream;
}
}Authorization
import { HttpModule, HttpService } from '@almvasiliev/http-client/nestjs';
import { BasicAuth, AuthInterceptor } from '@almvasiliev/http-client';
// Option 1: via defaultHeaders in forRoot
@Module({
imports: [
HttpModule.forRoot({
baseUrl: 'https://api.example.com',
defaultHeaders: {
Authorization: new BasicAuth({ username: 'admin', password: 'secret' }).getAuthHeader(),
},
}),
],
})
export class AppModule {}
// Option 2: via AuthInterceptor in a service
@Injectable()
export class ApiService {
constructor(private readonly httpService: HttpService) {
const auth = new BasicAuth({ username: 'admin', password: 'secret' });
this.httpService.addInterceptor(new AuthInterceptor(auth));
}
async getData() {
return this.httpService.get('/protected');
}
}
// Option 3: async configuration with credentials from ConfigService
HttpModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
baseUrl: config.get('API_URL'),
defaultHeaders: {
Authorization: new BasicAuth({
username: config.get('API_USERNAME'),
password: config.get('API_PASSWORD'),
}).getAuthHeader(),
},
}),
});Extension: custom Auth provider
Implement the HttpAuthProvider interface for any authorization type:
import {
HttpClient,
AuthInterceptor,
type HttpAuthProvider,
} from '@almvasiliev/http-client';
// Example: OAuth2 with automatic token refresh
class OAuth2Auth implements HttpAuthProvider {
private accessToken: string | null = null;
private expiresAt = 0;
constructor(
private readonly clientId: string,
private readonly clientSecret: string,
private readonly tokenUrl: string,
) {}
async getAuthHeader(): Promise<string> {
if (!this.accessToken || Date.now() >= this.expiresAt) {
await this.refreshToken();
}
return `Bearer ${this.accessToken}`;
}
private async refreshToken(): Promise<void> {
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
}),
});
const data = await response.json() as { access_token: string; expires_in: number };
this.accessToken = data.access_token;
this.expiresAt = Date.now() + data.expires_in * 1000 - 60_000; // refresh 1 min before expiry
}
}
// Usage
const auth = new OAuth2Auth('client-id', 'client-secret', 'https://auth.example.com/token');
const client = new HttpClient({ baseUrl: 'https://api.example.com' });
client.addInterceptor(new AuthInterceptor(auth));
// AuthInterceptor calls getAuthHeader() before each request,
// the token is refreshed automatically upon expiry
await client.get('/protected');// Example: API Key in a custom header
class ApiKeyAuth implements HttpAuthProvider {
constructor(
private readonly apiKey: string,
private readonly headerName = 'X-API-Key',
) {}
getAuthHeader(): string {
return this.apiKey;
}
}
// If you need a header other than Authorization, use an interceptor directly:
const client = new HttpClient({ baseUrl: 'https://api.example.com' });
client.addInterceptor({
onRequest(config) {
return {
...config,
headers: { ...config.headers, 'X-API-Key': 'my-api-key-123' },
};
},
});API
HttpClient / HttpService
| Method | Description |
|---|---|
| request<T>(config) | Full HTTP request |
| stream(config) | Streaming request, returns ReadableStream |
| get<T>(url, config?) | GET request |
| post<T>(url, data?, config?) | POST request |
| put<T>(url, data?, config?) | PUT request |
| patch<T>(url, data?, config?) | PATCH request |
| delete<T>(url, config?) | DELETE request |
| head(url, config?) | HEAD request |
| options<T>(url, config?) | OPTIONS request |
| addInterceptor(interceptor) | Add an interceptor |
Auth
| Class / Interface | Description |
|---|---|
| HttpAuthProvider | Authorization interface (getAuthHeader(): string \| Promise<string>) |
| BasicAuth | HTTP Basic Auth (Basic base64(username:password)) |
| BearerAuth | Bearer token (Bearer <token>) |
| AuthInterceptor | Interceptor for auto-injecting the Authorization header |
| BasicAuthCredentials | Type { username: string; password: string } |
Errors
| Class | Description |
|---|---|
| HttpRequestError | Base request error (extends Error, not HttpException) |
| HttpTimeoutError | Request timeout |
| HttpNetworkError | Network error |
Types
| Type | Description |
|---|---|
| HttpClientOptions | Options for native HttpClient |
| HttpModuleOptions | Options for NestJS HttpModule |
| HttpLoggingOptions | { all?: boolean; errors?: boolean; sanitizeHeaders?: string[] } |
| HttpRetryOptions | Retry: maxRetries, retryDelay, retryableStatuses, exponentialBackoff |
| HttpRequestConfig | Configuration for an individual request (supports per-request timeout and retry) |
| HttpResponse<T> | Server response |
| HttpStreamResponse | Streaming response with ReadableStream<Uint8Array> |
| HttpInterceptor | Interceptor interface |
| HttpLogger | Custom logger interface |
| HttpAuthProvider | Auth provider interface (getAuthHeader()) |
| BasicAuthCredentials | { username: string; password: string } |
Default Values
| Parameter | Value |
|---|---|
| timeout | 30000 ms |
| retry.maxRetries | 0 (retry disabled) |
| retry.retryDelay | 1000 ms |
| retry.retryableStatuses | [408, 429, 500, 502, 503, 504] |
| retry.exponentialBackoff | false |
| logging.all | false |
| logging.errors | true |
| logging.sanitizeHeaders | [] (no masking) |
| stream() timeout | 0 (no timeout) |
Requirements
- Node.js ≥ 18 (native fetch)
- NestJS ≥ 10 (for the NestJS module, optional dependency)
Type Definitions Included
This package includes:
*.d.tsfiles for all exported classes and functions- Type definitions for all parameters and return values
- IntelliSense support in VS Code and other TypeScript-aware editors
- Declaration maps for better debugging experience
