{"_id":"@almvasiliev/http-client","name":"@almvasiliev/http-client","dist-tags":{"latest":"1.0.0"},"versions":{"1.0.0":{"name":"@almvasiliev/http-client","version":"1.0.0","description":"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.","author":{"name":"Aleksey Vasiliev","email":"https://www.linkedin.com/in/almvasiliev/"},"license":"MIT","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.js"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","import":"./dist/nestjs/index.js","require":"./dist/nestjs/index.js"}},"keywords":["http","client","fetch","undici","nestjs","typescript","streaming","http-client","nestjs-http-client","typescript-http-client","openai-compatible"],"scripts":{"build":"tsc -p tsconfig.build.json","clean":"rm -rf dist","prepublishOnly":"npm run clean && npm run build","format":"biome format --write \"src/**/*.ts\"","lint":"biome check \"src/**/*.ts\"","lint:fix":"biome check --apply \"src/**/*.ts\"","test":"vitest run","test:watch":"vitest"},"peerDependencies":{"@nestjs/common":">=10.0.0","@nestjs/core":">=10.0.0","nestjs-pino":">=4.0.0","reflect-metadata":">=0.1.0"},"peerDependenciesMeta":{"@nestjs/common":{"optional":true},"@nestjs/core":{"optional":true},"nestjs-pino":{"optional":true},"reflect-metadata":{"optional":true}},"devDependencies":{"@biomejs/biome":"^2.2.2","@nestjs/common":"^11.0.0","@nestjs/core":"^11.0.0","@types/node":"^24.0.0","nestjs-pino":"^4.4.1","reflect-metadata":"^0.2.2","typescript":"^5.7.3","vitest":"^3.0.0"},"engines":{"node":">=18.0.0"},"_id":"@almvasiliev/http-client@1.0.0","_nodeVersion":"22.21.1","_npmVersion":"10.9.4","dist":{"integrity":"sha512-2GWmzU24DR+IyfUjLDAKVLR76afyNElpVMvfOjTxqDaDWOCzMU0PV9BDOZlsP27ca2tFLKf2h26G0Wbk3Fneeg==","shasum":"eb1faa0ca3bc3d65bd7c0edaa9b71be5d0fd4a4a","tarball":"https://registry.npmjs.org/@almvasiliev/http-client/-/http-client-1.0.0.tgz","fileCount":47,"unpackedSize":143542,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDyijyZW6W0T7zNqpOZpQogp6nulgCKCkVmXCF8xEWhWAIgfwxt+b+G8t0AnE92+ImrgUIwNDrZo+/IQGTrUJ4eVB8="}]},"_npmUser":{"name":"almvasiliev","email":"almvasiliev@gmail.com"},"directories":{},"maintainers":[{"name":"almvasiliev","email":"almvasiliev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/http-client_1.0.0_1772402844454_0.14786048679393193"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-01T22:07:24.391Z","1.0.0":"2026-03-01T22:07:24.606Z","modified":"2026-03-01T22:07:24.813Z"},"maintainers":[{"name":"almvasiliev","email":"almvasiliev@gmail.com"}],"description":"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.","keywords":["http","client","fetch","undici","nestjs","typescript","streaming","http-client","nestjs-http-client","typescript-http-client","openai-compatible"],"author":{"name":"Aleksey Vasiliev","email":"https://www.linkedin.com/in/almvasiliev/"},"license":"MIT","readme":"# @almvasiliev/http-client\n\nLightweight, 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.\n\n## Installation\n\n```bash\nnpm install @almvasiliev/http-client\n```\n\n## Features\n\n| Feature | Description |\n|---|---|\n| **Native fetch** | Uses built-in `fetch` (undici) |\n| **Streaming** | `ReadableStream<Uint8Array>` for OpenAI-compatible SSE interfaces |\n| **Timeout** | Configurable HTTP timeout (default 30 000 ms) |\n| **Retry** | Exponential backoff with configurable conditions and statuses |\n| **Interceptors** | Pipeline `onRequest → onResponse → onError` |\n| **Logging** | Independent control: all requests (default OFF), errors (default ON) |\n| **Sanitize headers** | Masking sensitive headers in logs (default list is empty) |\n| **Auth providers** | BasicAuth, BearerAuth and extensible `HttpAuthProvider` interface |\n| **Custom logger** | Pass your own logger via the `HttpLogger` interface |\n| **Dual entry** | Framework-agnostic (`./`) and NestJS (`./nestjs`) supports |\n\n---\n\n## Configuration\n\nAll parameters are passed when initializing the client (`HttpClient`) or the module (`HttpModule.forRoot()` / `forRootAsync()`).\n\n### Full parameter list\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `baseUrl` | `string` | — | Base URL for all requests |\n| `timeout` | `number` | `30000` | HTTP timeout in milliseconds |\n| `defaultHeaders` | `Record<string, string>` | — | Default headers for all requests |\n| `logging` | `boolean \\| HttpLoggingOptions` | `{ all: false, errors: true }` | Logging configuration (see below) |\n| `logger` | `HttpLogger` | Console | Custom logger (native `HttpClient` only) |\n| `retry` | `HttpRetryOptions` | see below | Retry configuration (see below) |\n\n### Retry (`retry`)\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `maxRetries` | `number` | `0` | Maximum number of retry attempts |\n| `retryDelay` | `number` | `1000` | Base delay between attempts (ms) |\n| `retryableStatuses` | `number[]` | `[408, 429, 500, 502, 503, 504]` | HTTP statuses that trigger a retry |\n| `exponentialBackoff` | `boolean` | `false` | Exponential delay growth |\n| `retryCondition` | `(error) => boolean` | — | Custom retry condition |\n| `onRetry` | `(count, error) => void` | — | Callback on each retry |\n\n### Logging (`logging`)\n\n| Parameter | Type | Default | Description |\n|---|---|---|---|\n| `all` | `boolean` | `false` | Log all requests and responses |\n| `errors` | `boolean` | `true` | Log errors |\n| `sanitizeHeaders` | `string[]` | `[]` | List of headers to mask in logs (case-insensitive) |\n\n**Shortcuts:**\n- `logging: true` — enable everything (`{ all: true, errors: true }`)\n- `logging: false` — disable everything (`{ all: false, errors: false }`)\n\n### Authorization (Auth Providers)\n\nThe package provides an extensible authorization system based on the `HttpAuthProvider` interface.\n\n### Built-in providers\n\n| Provider | Description |\n|---|---|\n| `BasicAuth` | HTTP Basic Authentication (`Basic base64(username:password)`) |\n| `BearerAuth` | Bearer token (`Bearer <token>`) |\n| `AuthInterceptor` | Interceptor for automatic injection of the `Authorization` header |\n\n---\n\n## Native usage\n\n```typescript\nimport { HttpClient } from '@almvasiliev/http-client';\n\nconst client = new HttpClient({\n  baseUrl: 'https://api.example.com',\n  timeout: 5000,\n  logging: {\n    all: true,\n    errors: true,\n    sanitizeHeaders: ['authorization', 'x-api-key'],\n  },\n  retry: {\n    maxRetries: 3,\n    retryDelay: 1000,\n    retryableStatuses: [408, 429, 500, 502, 503, 504],\n    exponentialBackoff: true,\n  },\n});\n\n// GET\nconst { data } = await client.get<User[]>('/users');\n\n// POST\nconst { data: created } = await client.post<User>('/users', { name: 'John' });\n\n// Streaming (OpenAI SSE)\nconst { stream, status } = await client.stream({\n  url: '/v1/chat/completions',\n  method: 'POST',\n  data: { model: 'gpt-4', messages: [...], stream: true },\n});\n\nconst reader = stream!.getReader();\nconst decoder = new TextDecoder();\nwhile (true) {\n  const { done, value } = await reader.read();\n  if (done) break;\n  console.log(decoder.decode(value));\n}\n```\n\n### Timeout\n\n```typescript\n// Global timeout of 10 seconds\nconst client = new HttpClient({ timeout: 10_000 });\n\n// Override at request level\nawait client.get('/slow-endpoint', { timeout: 60_000 });\n\n// No timeout (for streaming — default timeout: 0)\nconst { stream } = await client.stream({ url: '/v1/completions', method: 'POST', data: body });\n```\n\n### Retry\n\n```typescript\n// No retry (default)\nconst client = new HttpClient({ baseUrl: 'https://api.example.com' });\n\n// With retry: 3 attempts, exponential backoff\nconst client = new HttpClient({\n  baseUrl: 'https://api.example.com',\n  retry: {\n    maxRetries: 3,\n    retryDelay: 1000,\n    exponentialBackoff: true,\n  },\n});\n\n// Custom retryable statuses\nconst client = new HttpClient({\n  retry: {\n    maxRetries: 2,\n    retryableStatuses: [429, 503],\n  },\n});\n\n// Custom retry condition\nconst client = new HttpClient({\n  retry: {\n    maxRetries: 5,\n    retryCondition: (error) => error.status === 429,\n    onRetry: (count, error) => console.log(`Retry #${count}, status: ${error.status}`),\n  },\n});\n\n// Override retry at request level\nawait client.get('/unstable', {\n  retry: { maxRetries: 5, retryDelay: 2000 },\n});\n```\n\n### Logging: sanitizeHeaders\n\n```typescript\n// Masking sensitive headers in logs\nconst client = new HttpClient({\n  logging: {\n    all: true,\n    errors: true,\n    sanitizeHeaders: ['authorization', 'x-api-key', 'cookie', 'set-cookie'],\n  },\n});\n\n// In logs: headers: { authorization: '[REDACTED]', 'content-type': 'application/json' }\n```\n\n### Logging: control\n\n```typescript\n// Log everything (requests + responses + errors)\nnew HttpClient({ logging: true });\n\n// Log errors only (default)\nnew HttpClient({ logging: { all: false, errors: true } });\n\n// Everything disabled\nnew HttpClient({ logging: false });\n\n// Requests/responses only, no errors\nnew HttpClient({ logging: { all: true, errors: false } });\n```\n\n### Custom logger\n\n```typescript\nimport { HttpClient, type HttpLogger } from '@almvasiliev/http-client';\n\nconst myLogger: HttpLogger = {\n  log: (message, data) => console.info(`[HTTP] ${message}`, data),\n  error: (message, data) => console.error(`[HTTP ERROR] ${message}`, data),\n};\n\nconst client = new HttpClient({\n  baseUrl: 'https://api.example.com',\n  logging: {\n    all: true,\n    errors: true,\n    sanitizeHeaders: ['authorization'],\n  },\n  logger: myLogger,\n});\n```\n\n### Authorization BasicAuth\n\n```typescript\nimport { HttpClient, BasicAuth, AuthInterceptor } from '@almvasiliev/http-client';\n\nconst auth = new BasicAuth({ username: 'admin', password: 'secret' });\n\n// Option 1: via defaultHeaders\nconst client = new HttpClient({\n  baseUrl: 'https://api.example.com',\n  defaultHeaders: {\n    Authorization: auth.getAuthHeader(),\n  },\n});\n\n// Option 2: via AuthInterceptor (recommended)\nconst client2 = new HttpClient({ baseUrl: 'https://api.example.com' });\nclient2.addInterceptor(new AuthInterceptor(auth));\n\n// Option 3: per-request\nconst client3 = new HttpClient({ baseUrl: 'https://api.example.com' });\nawait client3.get('/protected', {\n  headers: { Authorization: auth.getAuthHeader() },\n});\n```\n\n### Authorization BearerAuth\n\n```typescript\nimport { HttpClient, BearerAuth, AuthInterceptor } from '@almvasiliev/http-client';\n\nconst auth = new BearerAuth('my-jwt-token');\n\nconst client = new HttpClient({ baseUrl: 'https://api.example.com' });\nclient.addInterceptor(new AuthInterceptor(auth));\n\nawait client.get('/protected'); // Authorization: Bearer my-jwt-token\n```\n\n---\n\n## NestJS usage\n\n```typescript\nimport { HttpModule, HttpService } from '@almvasiliev/http-client/nestjs';\n\n// app.module.ts — full configuration\n@Module({\n  imports: [\n    HttpModule.forRoot({\n      baseUrl: 'https://api.example.com',\n      timeout: 5000,\n      logging: {\n        all: false,\n        errors: true,\n        sanitizeHeaders: ['authorization', 'x-api-key'],\n      },\n      retry: {\n        maxRetries: 3,\n        retryDelay: 1000,\n        retryableStatuses: [408, 429, 500, 502, 503, 504],\n        exponentialBackoff: true,\n      },\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n### Async configuration\n\n```typescript\nHttpModule.forRootAsync({\n  imports: [ConfigModule],\n  inject: [ConfigService],\n  useFactory: (config: ConfigService) => ({\n    baseUrl: config.get('API_URL'),\n    timeout: Number(config.get('HTTP_TIMEOUT', 30000)),\n    logging: {\n      all: config.get('HTTP_LOG_ALL') === 'true',\n      errors: true,\n      sanitizeHeaders: ['authorization', 'x-api-key', 'cookie'],\n    },\n    retry: {\n      maxRetries: Number(config.get('HTTP_MAX_RETRIES', 0)),\n      retryDelay: Number(config.get('HTTP_RETRY_DELAY', 1000)),\n      retryableStatuses: [408, 429, 500, 502, 503, 504],\n      exponentialBackoff: true,\n    },\n  }),\n});\n```\n\n### Service usage\n\n```typescript\n@Injectable()\nexport class UserService {\n  constructor(private readonly httpService: HttpService) {}\n\n  async getUsers() {\n    return this.httpService.get<User[]>('/users');\n  }\n\n  async streamCompletion(messages: Message[]) {\n    const { stream } = await this.httpService.stream({\n      url: '/v1/chat/completions',\n      method: 'POST',\n      data: { model: 'gpt-4', messages, stream: true },\n    });\n    return stream;\n  }\n}\n```\n\n### Authorization\n\n```typescript\nimport { HttpModule, HttpService } from '@almvasiliev/http-client/nestjs';\nimport { BasicAuth, AuthInterceptor } from '@almvasiliev/http-client';\n\n// Option 1: via defaultHeaders in forRoot\n@Module({\n  imports: [\n    HttpModule.forRoot({\n      baseUrl: 'https://api.example.com',\n      defaultHeaders: {\n        Authorization: new BasicAuth({ username: 'admin', password: 'secret' }).getAuthHeader(),\n      },\n    }),\n  ],\n})\nexport class AppModule {}\n\n// Option 2: via AuthInterceptor in a service\n@Injectable()\nexport class ApiService {\n  constructor(private readonly httpService: HttpService) {\n    const auth = new BasicAuth({ username: 'admin', password: 'secret' });\n    this.httpService.addInterceptor(new AuthInterceptor(auth));\n  }\n\n  async getData() {\n    return this.httpService.get('/protected');\n  }\n}\n\n// Option 3: async configuration with credentials from ConfigService\nHttpModule.forRootAsync({\n  imports: [ConfigModule],\n  inject: [ConfigService],\n  useFactory: (config: ConfigService) => ({\n    baseUrl: config.get('API_URL'),\n    defaultHeaders: {\n      Authorization: new BasicAuth({\n        username: config.get('API_USERNAME'),\n        password: config.get('API_PASSWORD'),\n      }).getAuthHeader(),\n    },\n  }),\n});\n```\n\n---\n\n### Extension: custom Auth provider\n\nImplement the `HttpAuthProvider` interface for any authorization type:\n\n```typescript\nimport {\n  HttpClient,\n  AuthInterceptor,\n  type HttpAuthProvider,\n} from '@almvasiliev/http-client';\n\n// Example: OAuth2 with automatic token refresh\nclass OAuth2Auth implements HttpAuthProvider {\n  private accessToken: string | null = null;\n  private expiresAt = 0;\n\n  constructor(\n    private readonly clientId: string,\n    private readonly clientSecret: string,\n    private readonly tokenUrl: string,\n  ) {}\n\n  async getAuthHeader(): Promise<string> {\n    if (!this.accessToken || Date.now() >= this.expiresAt) {\n      await this.refreshToken();\n    }\n    return `Bearer ${this.accessToken}`;\n  }\n\n  private async refreshToken(): Promise<void> {\n    const response = await fetch(this.tokenUrl, {\n      method: 'POST',\n      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n      body: new URLSearchParams({\n        grant_type: 'client_credentials',\n        client_id: this.clientId,\n        client_secret: this.clientSecret,\n      }),\n    });\n    const data = await response.json() as { access_token: string; expires_in: number };\n    this.accessToken = data.access_token;\n    this.expiresAt = Date.now() + data.expires_in * 1000 - 60_000; // refresh 1 min before expiry\n  }\n}\n\n// Usage\nconst auth = new OAuth2Auth('client-id', 'client-secret', 'https://auth.example.com/token');\nconst client = new HttpClient({ baseUrl: 'https://api.example.com' });\nclient.addInterceptor(new AuthInterceptor(auth));\n\n// AuthInterceptor calls getAuthHeader() before each request,\n// the token is refreshed automatically upon expiry\nawait client.get('/protected');\n```\n\n```typescript\n// Example: API Key in a custom header\nclass ApiKeyAuth implements HttpAuthProvider {\n  constructor(\n    private readonly apiKey: string,\n    private readonly headerName = 'X-API-Key',\n  ) {}\n\n  getAuthHeader(): string {\n    return this.apiKey;\n  }\n}\n\n// If you need a header other than Authorization, use an interceptor directly:\nconst client = new HttpClient({ baseUrl: 'https://api.example.com' });\nclient.addInterceptor({\n  onRequest(config) {\n    return {\n      ...config,\n      headers: { ...config.headers, 'X-API-Key': 'my-api-key-123' },\n    };\n  },\n});\n```\n\n---\n\n## API\n\n### HttpClient / HttpService\n\n| Method | Description |\n|---|---|\n| `request<T>(config)` | Full HTTP request |\n| `stream(config)` | Streaming request, returns `ReadableStream` |\n| `get<T>(url, config?)` | GET request |\n| `post<T>(url, data?, config?)` | POST request |\n| `put<T>(url, data?, config?)` | PUT request |\n| `patch<T>(url, data?, config?)` | PATCH request |\n| `delete<T>(url, config?)` | DELETE request |\n| `head(url, config?)` | HEAD request |\n| `options<T>(url, config?)` | OPTIONS request |\n| `addInterceptor(interceptor)` | Add an interceptor |\n\n### Auth\n\n| Class / Interface | Description |\n|---|---|\n| `HttpAuthProvider` | Authorization interface (`getAuthHeader(): string \\| Promise<string>`) |\n| `BasicAuth` | HTTP Basic Auth (`Basic base64(username:password)`) |\n| `BearerAuth` | Bearer token (`Bearer <token>`) |\n| `AuthInterceptor` | Interceptor for auto-injecting the `Authorization` header |\n| `BasicAuthCredentials` | Type `{ username: string; password: string }` |\n\n### Errors\n\n| Class | Description |\n|---|---|\n| `HttpRequestError` | Base request error (extends `Error`, not `HttpException`) |\n| `HttpTimeoutError` | Request timeout |\n| `HttpNetworkError` | Network error |\n\n### Types\n\n| Type | Description |\n|---|---|\n| `HttpClientOptions` | Options for native `HttpClient` |\n| `HttpModuleOptions` | Options for NestJS `HttpModule` |\n| `HttpLoggingOptions` | `{ all?: boolean; errors?: boolean; sanitizeHeaders?: string[] }` |\n| `HttpRetryOptions` | Retry: `maxRetries`, `retryDelay`, `retryableStatuses`, `exponentialBackoff` |\n| `HttpRequestConfig` | Configuration for an individual request (supports per-request `timeout` and `retry`) |\n| `HttpResponse<T>` | Server response |\n| `HttpStreamResponse` | Streaming response with `ReadableStream<Uint8Array>` |\n| `HttpInterceptor` | Interceptor interface |\n| `HttpLogger` | Custom logger interface |\n| `HttpAuthProvider` | Auth provider interface (`getAuthHeader()`) |\n| `BasicAuthCredentials` | `{ username: string; password: string }` |\n\n---\n\n## Default Values\n\n| Parameter | Value |\n|---|---|\n| `timeout` | `30000` ms |\n| `retry.maxRetries` | `0` (retry disabled) |\n| `retry.retryDelay` | `1000` ms |\n| `retry.retryableStatuses` | `[408, 429, 500, 502, 503, 504]` |\n| `retry.exponentialBackoff` | `false` |\n| `logging.all` | `false` |\n| `logging.errors` | `true` |\n| `logging.sanitizeHeaders` | `[]` (no masking) |\n| `stream()` timeout | `0` (no timeout) |\n\n## Requirements\n\n- **Node.js ≥ 18** (native fetch)\n- **NestJS ≥ 10** (for the NestJS module, optional dependency)\n\n## Type Definitions Included\n\nThis package includes:\n- `*.d.ts` files for all exported classes and functions\n- Type definitions for all parameters and return values\n- IntelliSense support in VS Code and other TypeScript-aware editors\n- Declaration maps for better debugging experience\n","readmeFilename":"README.md","_rev":"1-a285b701d461e51ec61ca939c626b409"}