{"_id":"@asaidimu/network-client","_rev":"2-ee473db646bbe33a3ef484823479f57b","name":"@asaidimu/network-client","dist-tags":{"latest":"2.0.0"},"versions":{"1.0.0":{"name":"@asaidimu/network-client","version":"1.0.0","keywords":["http","client","fetch","typescript","browser"],"author":{"name":"saidimu"},"license":"MIT","_id":"@asaidimu/network-client@1.0.0","maintainers":[{"name":"asaidimu","email":"lolokilesaidimu@gmail.com"}],"homepage":"https://github.com/asaidimu/network-client#readme","bugs":{"url":"https://github.com/asaidimu/network-client/issues"},"dist":{"shasum":"8c807bce73552fc6866e2f323e762911af524199","tarball":"https://registry.npmjs.org/@asaidimu/network-client/-/network-client-1.0.0.tgz","fileCount":7,"integrity":"sha512-dUABqAMTX4ak87eiGEhSh8ZK5sYoEiCjkYU0jOuFe2ayX/jLUkz3JuEI/hnu6/1D1nEyQEJeq6TlreY9/a9FDw==","signatures":[{"sig":"MEYCIQC7qaa4XAKfy9BoQSukbgj/PDOczZdW0Il1k2wg3A8ugQIhAN8sZtfiSsulrfzgVaeoHhk8WRN7BzLfWF6j6o0Puj9o","keyid":"SHA256:jl3bwswu80PjjokCgh0o2w5c2U4LhQAE57gj9cz1kzA"}],"unpackedSize":19935},"main":"index.js","types":"index.d.ts","module":"index.mjs","gitHead":"dcc0ebe61ba917680997fb07c01f51b6b9b98658","_npmUser":{"name":"asaidimu","email":"lolokilesaidimu@gmail.com"},"repository":{"url":"git+ssh://git@github.com/asaidimu/network-client.git","type":"git"},"_npmVersion":"10.5.2","description":"A lightweight, type-safe HTTP client for browser environments","directories":{},"_nodeVersion":"20.13.1","publishConfig":{"tag":"latest","access":"public","registry":"https://registry.npmjs.org/"},"_hasShrinkwrap":false,"devDependencies":{"typescript":"^5.0.0"},"_npmOperationalInternal":{"tmp":"tmp/network-client_1.0.0_1730186364264_0.9704382199665706","host":"s3://npm-registry-packages"}},"2.0.0":{"name":"@asaidimu/network-client","version":"2.0.0","description":"A lightweight, type-safe HTTP client for browser environments","main":"index.js","module":"index.mjs","types":"index.d.ts","keywords":["http","client","fetch","typescript","browser"],"author":{"name":"saidimu"},"repository":{"type":"git","url":"git+ssh://git@github.com/asaidimu/network-client.git"},"license":"MIT","devDependencies":{"typescript":"^5.0.0"},"publishConfig":{"registry":"https://registry.npmjs.org/","tag":"latest","access":"public"},"_id":"@asaidimu/network-client@2.0.0","gitHead":"efe30830cfa86b87c1c23f9f4fc9b13555ea0749","bugs":{"url":"https://github.com/asaidimu/network-client/issues"},"homepage":"https://github.com/asaidimu/network-client#readme","_nodeVersion":"20.19.1","_npmVersion":"10.8.2","dist":{"integrity":"sha512-r4nykxPfxga9DO9SBZZP76s4LOKEIyjIJYt+a/4FLuCmKTxMDk7BKXVtdu7Xj1IFpGPmcmss8VCLm7M/ioPJVQ==","shasum":"7e1182d4fbc738591c8a58acd5f994837178507f","tarball":"https://registry.npmjs.org/@asaidimu/network-client/-/network-client-2.0.0.tgz","fileCount":7,"unpackedSize":83533,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCwgunza0xj6tfrYzu1enPHRCUzHx4RrHcrb/zqZ7Np0wIhAMBQxy9tfctFw0cwtlO1Bb631AEjovcEyBHd2eXST1Sl"}]},"_npmUser":{"name":"asaidimu","email":"lolokilesaidimu@gmail.com"},"directories":{},"maintainers":[{"name":"asaidimu","email":"lolokilesaidimu@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/network-client_2.0.0_1748073970478_0.3381433044057516"},"_hasShrinkwrap":false}},"time":{"created":"2024-10-29T07:19:24.047Z","modified":"2025-05-24T08:06:10.911Z","1.0.0":"2024-10-29T07:19:24.415Z","2.0.0":"2025-05-24T08:06:10.701Z"},"bugs":{"url":"https://github.com/asaidimu/network-client/issues"},"author":{"name":"saidimu"},"license":"MIT","homepage":"https://github.com/asaidimu/network-client#readme","keywords":["http","client","fetch","typescript","browser"],"repository":{"type":"git","url":"git+ssh://git@github.com/asaidimu/network-client.git"},"description":"A lightweight, type-safe HTTP client for browser environments","maintainers":[{"name":"asaidimu","email":"lolokilesaidimu@gmail.com"}],"readme":"# @asaidimu/network-client\n\nA lightweight, type-safe HTTP client for browser environments with\npowerful middleware, retry, and caching capabilities, designed for\nbuilding robust API SDKs.\n\n[![npm version](https://img.shields.io/npm/v/@asaidimu/network-client.svg?style=flat-square)](https://www.npmjs.com/package/@asaidimu/network-client)\n[![npm license](https://img.shields.io/npm/l/@asaidimu/network-client.svg?style=flat-square)](https://www.npmjs.com/package/@asaidimu/network-client)\n[![Build Status](https://img.shields.io/github/actions/workflow/status/asaidimu/network-client/ci.yml?branch=main&label=Build&style=flat-square)](https://github.com/asaidimu/network-client/actions/workflows/ci.yml)\n\n---\n\n## 📚 Table of Contents\n\n*   [Overview & Features](#-overview--features)\n*   [Installation](#-installation)\n*   [Quick Start](#-quick-start)\n*   [Configuration](#-configuration)\n*   [Making Requests](#-making-requests)\n    *   [HTTP Methods](#http-methods)\n    *   [Request Options](#request-options)\n    *   [Body Serialization](#body-serialization)\n    *   [Response Parsing](#response-parsing)\n*   [Response Structure](#-response-structure)\n*   [Middleware & Interceptors](#-middleware--interceptors)\n*   [Retry Mechanism](#-retry-mechanism)\n*   [Client-Side Caching](#-client-side-caching)\n*   [Building SDKs](#-building-sdks)\n*   [Error Handling](#-error-handling)\n*   [TypeScript Support](#-typescript-support)\n*   [Project Architecture](#-project-architecture)\n*   [Development & Contributing](#-development--contributing)\n*   [Additional Information](#-additional-information)\n\n---\n\n## 💡 Overview & Features\n\n`@asaidimu/network-client` is a modern, promise-based HTTP client\nmeticulously crafted for browser environments. It leverages the native\n`fetch` API, providing a type-safe and highly configurable solution for\ninteracting with RESTful APIs. Beyond basic request functionality, this\nclient introduces a robust middleware system, intelligent request/response\nhandling, configurable retry logic, and an opt-in caching mechanism,\nmaking it an ideal foundation for building scalable and maintainable API\nintegrations or even full-fledged SDKs.\n\nThis library aims to simplify complex network operations by providing\na clean, intuitive API while exposing powerful extension points. Whether\nyou need fine-grained control over request headers, custom body\nserialization, automated retries for transient failures, or a shared\ncaching layer, `@asaidimu/network-client` delivers the flexibility and\nreliability required for modern web applications.\n\n### ✨ Key Features\n\n*   **Comprehensive Type Safety**: Full TypeScript support with detailed\n    interfaces for requests, responses, errors, and configuration.\n*   **Promise-Based API**: Asynchronous operations handled elegantly with\n    `async/await`.\n*   **Flexible Configuration**: Set base URLs, default headers, and\n    timeouts at the client level.\n*   **Extensible Middleware System**: Intercept and modify requests,\n    responses, and errors at various stages of the lifecycle.\n*   **Smart Body Serialization**: Automatic detection and serialization\n    for JSON, FormData, URLSearchParams, text, blob, and streams.\n*   **Intelligent Response Parsing**: Automatically parses JSON, text,\n    blob, ArrayBuffer, or FormData based on `Content-Type` headers, with\n    explicit override options.\n*   **Configurable Retry Logic**: Implement robust retry strategies with\n    exponential or linear backoff for transient network issues.\n*   **Built-in Client-Side Caching**: Simple, configurable cache for GET\n    requests with Time-To-Live (TTL).\n*   **Dedicated SDK Base Class**: Provides a `BaseSDK` class to simplify\n    the creation of modular and reusable API SDKs.\n*   **Extendable Client Instances**: Clone and extend existing client\n    configurations to create specialized instances without modifying the\n    original.\n*   **Abort Signal Integration**: Native support for `AbortSignal` for\n    request cancellation and timeout handling.\n*   **Raw Response Access**: Provides access to the underlying `Response`\n    object for advanced use cases (e.g., streaming).\n\n---\n\n## 🚀 Installation\n\n### Prerequisites\n\n*   Node.js (LTS version recommended)\n*   A modern browser environment for client-side usage.\n\n### Installation Steps\n\nInstall the package using your preferred package manager:\n\n```bash\n# Using npm\nnpm install @asaidimu/network-client\n\n# Using yarn\nyarn add @asaidimu/network-client\n\n# Using pnpm\npnpm add @asaidimu/network-client\n\n# Using Bun\nbun add @asaidimu/network-client\n```\n\n### Verification\n\nTo verify the installation, you can create a simple TypeScript file (e.g., `test.ts`):\n\n```typescript\n// test.ts\nimport { createNetworkClient } from '@asaidimu/network-client';\n\nconst client = createNetworkClient({\n  baseUrl: 'https://jsonplaceholder.typicode.com', // A public test API\n});\n\nasync function runTest() {\n  try {\n    const response = await client.get<{ title: string }>('/todos/1');\n    if (response.success) {\n      console.log('Client initialized and request successful!');\n      console.log('Data:', response.data);\n    } else {\n      console.error('Request failed:', response.error);\n    }\n  } catch (error) {\n    console.error('An unexpected error occurred:', error);\n  }\n}\n\nrunTest();\n```\n\nCompile and run:\n\n```bash\n# First, ensure you have TypeScript installed globally or locally\n# npm install -g typescript\n# Then, compile the test file\nnpx ts-node test.ts\n# or\n# tsc test.ts && node test.js\n```\n\nYou should see output indicating a successful request to `jsonplaceholder.typicode.com`.\n\n---\n\n## ⚡ Quick Start\n\nGet up and running with a basic client configuration and a simple request:\n\n```typescript\nimport { createNetworkClient, ApiResponse, RequestError } from '@asaidimu/network-client';\n\ninterface User {\n  id: number;\n  name: string;\n  email: string;\n}\n\n// 1. Create a client instance\nconst apiClient = createNetworkClient({\n  baseUrl: 'https://api.example.com/v1',\n  defaultHeaders: {\n    'Authorization': 'Bearer YOUR_API_TOKEN',\n    'X-Requested-With': 'XMLHttpRequest',\n  },\n  defaultTimeout: 10000, // 10 seconds\n});\n\n// 2. Make a request\nasync function fetchUsers(): Promise<void> {\n  console.log('Fetching users...');\n  const response: ApiResponse<User[]> = await apiClient.get<User[]>('/users');\n\n  if (response.success) {\n    console.log('Users fetched successfully:', response.data);\n    // You can access raw response details if needed\n    console.log('Status:', response.status);\n    console.log('Headers:', response.headers.get('Content-Type'));\n  } else {\n    const error: RequestError | undefined = response.error;\n    console.error('Failed to fetch users:', error?.message);\n    if (error?.status) {\n      console.error('Status Code:', error.status);\n    }\n  }\n}\n\n// 3. Call the function\nfetchUsers();\n\n// Example of a POST request\nasync function createUser(): Promise<void> {\n  console.log('Creating new user...');\n  const newUser = { name: 'Alice Smith', email: 'alice.s@example.com' };\n  const response: ApiResponse<User> = await apiClient.post<User>('/users', newUser);\n\n  if (response.success) {\n    console.log('User created:', response.data);\n  } else {\n    console.error('Failed to create user:', response.error?.message);\n  }\n}\n\n// createUser(); // Uncomment to run this example\n```\n\n---\n\n## ⚙️ Configuration\n\nThe `createNetworkClient` function accepts a `NetworkClientConfig` object to define the client's behavior:\n\n```typescript\nimport { NetworkClientConfig, HttpMethod, RequestError } from '@asaidimu/network-client';\n\ninterface RetryConfig {\n  attempts: number; // Number of retry attempts\n  delay: number; // Initial delay in milliseconds\n  backoff?: 'linear' | 'exponential'; // Backoff strategy (default: 'linear')\n  retryCondition?: (error: RequestError, attempt: number) => boolean; // Custom retry condition\n}\n\ninterface CacheConfig {\n  ttl: number; // Time to live in milliseconds\n  key?: (url: string, method: HttpMethod, body?: unknown) => string; // Custom cache key generator\n}\n\ninterface Middleware {\n  onRequest?(context: RequestContext): RequestContext | Promise<RequestContext>;\n  onResponse?<T>(context: ResponseContext<T>): ResponseContext<T> | Promise<ResponseContext<T>>;\n  onError?(error: RequestError): RequestError | Promise<RequestError>;\n}\n\ntype RequestInterceptor = (context: RequestContext) => RequestContext | Promise<RequestContext>;\ntype ResponseInterceptor<T = unknown> = (context: ResponseContext<T>) => ResponseContext<T> | Promise<ResponseContext<T>>;\n\ntype BodyType = 'json' | 'form' | 'text' | 'blob' | 'stream' | 'auto';\ntype ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData' | 'auto';\n\ninterface NetworkClientConfig {\n  baseUrl: string; // Base URL for all requests (e.g., 'https://api.example.com/v1')\n  defaultHeaders?: Record<string, string>; // Headers applied to all requests by default\n  defaultTimeout?: number; // Default request timeout in milliseconds\n  middleware?: Middleware[]; // Array of middleware objects\n  retry?: RetryConfig; // Retry configuration\n  cache?: CacheConfig; // Caching configuration for GET requests\n  interceptors?: { // Fine-grained interceptors\n    request?: RequestInterceptor[];\n    response?: ResponseInterceptor[];\n  };\n  defaultResponseType?: ResponseType; // Default way to parse responses (e.g., 'json', 'text', 'auto')\n  defaultBodyType?: BodyType; // Default way to serialize request bodies (e.g., 'json', 'form', 'auto')\n}\n```\n\nExample Configuration:\n\n```typescript\nimport { createNetworkClient } from '@asaidimu/network-client';\n\nconst myClient = createNetworkClient({\n  baseUrl: 'https://myapi.service.com',\n  defaultHeaders: {\n    'Accept': 'application/json',\n    'User-Agent': 'My-App/1.0',\n  },\n  defaultTimeout: 8000, // 8 seconds\n  defaultResponseType: 'json', // Always try to parse as JSON by default\n  defaultBodyType: 'json', // Always try to serialize body as JSON by default\n  retry: {\n    attempts: 3,\n    delay: 1000, // 1 second initial delay\n    backoff: 'exponential', // Delays: 1s, 2s, 4s\n    retryCondition: (error, attempt) => {\n      // Only retry on 5xx errors or specific network issues\n      return (error.status && error.status >= 500) || error.message.includes('NetworkError');\n    }\n  },\n  cache: {\n    ttl: 60 * 1000, // Cache GET responses for 60 seconds\n    key: (url, method, body) => {\n      // Custom cache key for GET requests, ignoring body for simplicity\n      return `${method}:${url}`;\n    }\n  }\n});\n```\n\n---\n\n## ⚡ Making Requests\n\nThe client provides methods for all standard HTTP verbs.\n\n### HTTP Methods\n\n```typescript\nimport { createNetworkClient, ApiResponse, RequestOptions, BodyOptions } from '@asaidimu/network-client';\n\nconst client = createNetworkClient({ baseUrl: 'https://api.example.com' });\n\n// GET request\nasync function getResource() {\n  const response: ApiResponse<any> = await client.get('/resources');\n  if (response.success) {\n    console.log('GET Data:', response.data);\n  }\n}\n\n// POST request with a JSON body (default behavior for objects)\nasync function createResource() {\n  const newResource = { name: 'New Item', value: 123 };\n  const response: ApiResponse<any> = await client.post('/resources', newResource);\n  if (response.success) {\n    console.log('POST Response:', response.data);\n  }\n}\n\n// PUT request with body and custom options\nasync function updateResource(id: string, data: any) {\n  const options: RequestOptions = {\n    headers: { 'X-Custom-Header': 'update-flow' },\n    timeout: 5000,\n  };\n  const response: ApiResponse<any> = await client.put(`/resources/${id}`, data, options);\n  if (response.success) {\n    console.log('PUT Response:', response.data);\n  }\n}\n\n// PATCH request with body and explicit body options\nasync function partiallyUpdateResource(id: string, patchData: any) {\n  const bodyOptions: BodyOptions = { type: 'json' }; // Explicitly send as JSON\n  const response: ApiResponse<any> = await client.patch(`/resources/${id}`, patchData, {}, bodyOptions);\n  if (response.success) {\n    console.log('PATCH Response:', response.data);\n  }\n}\n\n// DELETE request\nasync function deleteResource(id: string) {\n  const response: ApiResponse<void> = await client.delete(`/resources/${id}`);\n  if (response.success) {\n    console.log('DELETE Success:', response.status); // 204 No Content for successful deletion\n  }\n}\n\n// HEAD request (fetches headers only)\nasync function getHeaders() {\n  const response: ApiResponse<void> = await client.head('/status');\n  if (response.success) {\n    console.log('HEAD Headers:', response.headers);\n    console.log('Content-Type:', response.headers.get('Content-Type'));\n  }\n}\n\n// OPTIONS request (fetches allowed methods)\nasync function getOptions() {\n  const response: ApiResponse<void> = await client.options('/resources');\n  if (response.success) {\n    console.log('OPTIONS Allowed methods:', response.headers.get('Allow'));\n  }\n}\n\n// Generic request method (for dynamic method calls)\nasync function makeGenericRequest(method: 'GET' | 'POST', path: string, body?: unknown) {\n  const response = await client.request(method, path, body);\n  if (response.success) {\n    console.log(`${method} Generic Response:`, response.data);\n  }\n}\n```\n\n### Request Options\n\nRequests can accept an `options` object for fine-grained control:\n\n```typescript\ninterface RequestOptions {\n  headers?: Record<string, string>; // Custom headers for this request\n  timeout?: number; // Override default timeout for this request in ms\n  signal?: AbortSignal; // AbortSignal for manual cancellation\n  responseType?: ResponseType; // Explicitly define how to parse the response\n  expectJson?: boolean; // Shorthand for responseType: 'json'\n}\n```\n\nExample with `AbortSignal` for cancellation:\n\n```typescript\nimport { createNetworkClient } from '@asaidimu/network-client';\n\nconst client = createNetworkClient({ baseUrl: 'https://api.example.com' });\n\nasync function fetchWithCancellation() {\n  const controller = new AbortController();\n  const signal = controller.signal;\n\n  // Simulate user leaving page or timeout on client side\n  setTimeout(() => controller.abort(), 2000); // Cancel after 2 seconds\n\n  try {\n    const response = await client.get('/long-running-task', { signal });\n    if (response.success) {\n      console.log('Task completed:', response.data);\n    } else {\n      console.error('Task failed:', response.error?.message);\n    }\n  } catch (error: any) {\n    if (error.name === 'AbortError') {\n      console.warn('Request was aborted!');\n    } else {\n      console.error('An error occurred:', error.message);\n    }\n  }\n}\n\nfetchWithCancellation();\n```\n\n### Body Serialization\n\nThe client automatically detects and serializes request bodies based on content. You can explicitly control this with `bodyOptions`:\n\n```typescript\ninterface BodyOptions {\n  type?: BodyType; // Explicitly set the body type ('json', 'form', 'text', 'blob', 'stream', 'auto')\n  contentType?: string; // Manually specify Content-Type header (overrides type inference)\n}\n```\n\nExample Usage:\n\n```typescript\nimport { createNetworkClient } from '@asaidimu/network-client';\n\nconst client = createNetworkClient({ baseUrl: 'https://api.example.com' });\n\n// Default: JSON for objects (implicit type: 'json')\nawait client.post('/users', { name: 'John Doe', email: 'john@example.com' });\n\n// Form data (multipart/form-data for FormData, application/x-www-form-urlencoded for URLSearchParams/objects)\nconst formData = new FormData();\nformData.append('file', new Blob(['hello'], { type: 'text/plain' }), 'hello.txt');\nawait client.post('/upload', formData, {}, { type: 'form' }); // 'type: form' ensures correct handling\n\n// URL-encoded form data from an object\nawait client.post('/submit-form', { param1: 'value1', param2: 'value2' }, {}, { type: 'form' });\n\n// Plain text body\nawait client.post('/log', 'This is a raw log entry.', {}, { type: 'text' });\n\n// Blob/ArrayBuffer/Stream (e.g., for binary uploads)\nconst binaryData = new ArrayBuffer(8); // Example binary data\nawait client.post('/binary-upload', binaryData, {}, { type: 'blob', contentType: 'application/octet-stream' });\n```\n\n### Response Parsing\n\nThe client attempts to parse responses based on the `Content-Type` header. You can override this with `responseType` in `RequestOptions` or `expectJson`:\n\n```typescript\nimport { createNetworkClient } from '@asaidimu/network-client';\n\nconst client = createNetworkClient({ baseUrl: 'https://api.example.com' });\n\n// Auto-detection (default): Parses based on Content-Type header\nconst autoResponse = await client.get('/data');\n\n// Explicitly expect JSON (shorthand)\ninterface User { id: number; name: string; }\nconst usersJson = await client.get<User[]>('/users', { expectJson: true });\n\n// Explicitly expect text\nconst rawText = await client.get<string>('/document.txt', { responseType: 'text' });\n\n// Explicitly expect Blob (e.g., for image download)\nconst imageBlob = await client.get<Blob>('/image.png', { responseType: 'blob' });\nif (imageBlob.success && imageBlob.data) {\n  const imageUrl = URL.createObjectURL(imageBlob.data);\n  console.log('Image URL:', imageUrl);\n}\n\n// Explicitly expect ArrayBuffer (e.g., for binary data processing)\nconst audioBuffer = await client.get<ArrayBuffer>('/audio.mp3', { responseType: 'arrayBuffer' });\n\n// Explicitly expect FormData (e.g., for specific API responses)\nconst formResponse = await client.get<FormData>('/form-data-endpoint', { responseType: 'formData' });\n```\n\n---\n\n## 📦 Response Structure\n\nAll client methods return an `ApiResponse<T>` object, providing a consistent way to handle responses, including data, errors, and metadata.\n\n```typescript\ninterface RequestError {\n  message: string;\n  status?: number; // HTTP status code (e.g., 404, 500)\n  url?: string;\n  method?: HttpMethod;\n}\n\ninterface ApiResponse<T> {\n  data?: T; // The parsed response data (if successful)\n  error?: RequestError; // Error details (if request failed or parsing issue)\n  success: boolean; // True if the request was successful (HTTP status 2xx)\n  status: number; // The HTTP status code\n  headers: Headers; // The raw Fetch API Headers object\n  raw?: Response; // The raw Fetch API Response object for advanced use cases\n}\n```\n\nExample usage:\n\n```typescript\nimport { createNetworkClient, ApiResponse, RequestError } from '@asaidimu/network-client';\n\nconst client = createNetworkClient({ baseUrl: 'https://api.example.com' });\n\nasync function handleResponse() {\n  const response: ApiResponse<{ message: string }> = await client.get('/some-endpoint');\n\n  if (response.success) {\n    console.log('Data:', response.data?.message);\n    console.log('HTTP Status:', response.status);\n    console.log('Content-Type Header:', response.headers.get('content-type'));\n    if (response.raw) {\n      console.log('Raw response URL:', response.raw.url);\n    }\n  } else {\n    const error: RequestError | undefined = response.error;\n    console.error('Request failed!');\n    console.error('Error Message:', error?.message);\n    console.error('Error Status:', error?.status);\n    console.error('Request URL:', error?.url);\n    console.error('Request Method:', error?.method);\n  }\n}\n```\n\n---\n\n## ♻️ Middleware & Interceptors\n\nThe client provides two powerful mechanisms for extending its functionality: `Middleware` and `Interceptors`.\n\n*   **Interceptors**: Functions (`RequestInterceptor`,\n    `ResponseInterceptor`) that get called sequentially before a request\n    is sent or after a response is received. They directly modify the\n    `RequestContext` or `ResponseContext`.\n*   **Middleware**: Objects (`Middleware`) with optional `onRequest`,\n    `onResponse`, and `onError` methods, offering a structured way to\n    handle different phases of the request lifecycle. Middleware functions\n    are also executed sequentially.\n\nBoth systems allow you to implement common patterns like logging, authentication, error reporting, and data transformation.\n\n```typescript\nimport {\n  createNetworkClient,\n  Middleware,\n  RequestInterceptor,\n  ResponseInterceptor,\n  RequestContext,\n  ResponseContext,\n  RequestError\n} from '@asaidimu/network-client';\n\n// 1. Define a Middleware object\nconst authMiddleware: Middleware = {\n  async onRequest(context: RequestContext): Promise<RequestContext> {\n    const token = localStorage.getItem('authToken');\n    if (token) {\n      context.headers = { ...context.headers, 'Authorization': `Bearer ${token}` };\n    }\n    return context;\n  },\n  async onError(error: RequestError): Promise<RequestError> {\n    if (error.status === 401) {\n      console.error('Authentication failed, redirecting to login...');\n      // Example: window.location.href = '/login';\n    }\n    return error; // Always return the (potentially modified) error\n  }\n};\n\n// 2. Define Request and Response Interceptors\nconst loggingInterceptor: RequestInterceptor = async (context: RequestContext) => {\n  console.log(`[Request Interceptor] ${context.method} ${context.url}`);\n  console.log('Headers:', context.headers);\n  return context;\n};\n\nconst responseLoggerInterceptor: ResponseInterceptor = async (context: ResponseContext<any>) => {\n  console.log(`[Response Interceptor] Status: ${context.response.status}`);\n  if (context.data) {\n    console.log('Data (from interceptor):', context.data);\n  }\n  if (context.error) {\n    console.error('Error (from interceptor):', context.error.message);\n  }\n  return context;\n};\n\n// 3. Initialize the client with middleware and interceptors\nconst client = createNetworkClient({\n  baseUrl: 'https://api.example.com',\n  middleware: [authMiddleware], // Add middleware during client creation\n  interceptors: {\n    request: [loggingInterceptor],\n    response: [responseLoggerInterceptor]\n  }\n});\n\n// You can also add interceptors after client creation\nclient.addInterceptor('request', async (context) => {\n  console.log('[Interceptor Added Later] Another request interceptor!');\n  return context;\n});\n\n// Example of a response interceptor transforming data\nclient.addInterceptor('response', async (context) => {\n  if (context.response.ok && context.data && typeof context.data === 'object' && 'results' in context.data) {\n    // If the API wraps data in a 'results' field, unwrap it\n    context.data = (context.data as { results: any }).results;\n  }\n  return context;\n});\n```\n\n---\n\n## 🔄 Retry Mechanism\n\nThe client includes a built-in retry mechanism for handling transient\nnetwork failures or server errors. Configure it via the `retry` option in\n`NetworkClientConfig`.\n\n```typescript\nimport { createNetworkClient } from '@asaidimu/network-client';\n\nconst clientWithRetry = createNetworkClient({\n  baseUrl: 'https://api.example.com',\n  retry: {\n    attempts: 3,        // Total number of attempts (1 initial + 2 retries)\n    delay: 1000,        // Initial delay before the first retry (1 second)\n    backoff: 'exponential', // 'linear' (delay * attempt) or 'exponential' (delay * 2^(attempt-1))\n    retryCondition: (error, attempt) => {\n      // Custom condition: retry on network errors (status 0) or 5xx server errors\n      // and only for the first 2 retries (attempts 1, 2)\n      return (!error.status || error.status >= 500) && attempt < 3;\n    }\n  }\n});\n\n// Example: This request will automatically retry if it fails due to a network issue or 5xx error\nasync function fetchDataWithRetry() {\n  console.log('Attempting to fetch data with retry...');\n  const response = await clientWithRetry.get('/unreliable-endpoint');\n  if (response.success) {\n    console.log('Data fetched successfully after potential retries:', response.data);\n  } else {\n    console.error('Failed to fetch data after all retries:', response.error?.message);\n  }\n}\n\nfetchDataWithRetry();\n```\n\n---\n\n## 💾 Client-Side Caching\n\nFor `GET` requests, you can enable a simple in-memory cache to reduce redundant network calls and improve performance.\n\n```typescript\nimport { createNetworkClient } from '@asaidimu/network-client';\n\nconst clientWithCache = createNetworkClient({\n  baseUrl: 'https://api.example.com',\n  cache: {\n    ttl: 5 * 60 * 1000, // Cache entries for 5 minutes (in milliseconds)\n    key: (url, method, body) => {\n      // Optional: Custom cache key generation. Default is `${method}:${url}`.\n      // If your URL has query params that don't affect cache, you might normalize it.\n      // Example: return `${method}:${new URL(url).pathname}`;\n      return `${method}:${url}`;\n    }\n  }\n});\n\nasync function fetchCachedData() {\n  // First request: hits the network, stores in cache\n  console.log('Fetching users (first time, from network)...');\n  let response1 = await clientWithCache.get<{ id: number; name: string }[]>('/users');\n  console.log('Response 1 status:', response1.status); // Will be 200\n\n  // Second request: hits the cache if within TTL\n  console.log('Fetching users (second time, from cache if available)...');\n  let response2 = await clientWithCache.get<{ id: number; name: string }[]>('/users');\n  console.log('Response 2 status:', response2.status); // Will be 200 (from cache)\n\n  // Clear the cache manually if needed (e.g., after a mutation)\n  clientWithCache.clearCache();\n  console.log('Cache cleared.');\n\n  // Third request: will hit the network again\n  console.log('Fetching users (third time, after cache clear)...');\n  let response3 = await clientWithCache.get<{ id: number; name: string }[]>('/users');\n  console.log('Response 3 status:', response3.status); // Will be 200\n\n  // You can also delete specific items from the cache\n  // cache.delete('GET:https://api.example.com/users'); // Not exposed via client currently, use client.clearCache()\n}\n\nfetchCachedData();\n```\n\n---\n\n## 🏗️ Building SDKs\n\nThe library provides a `BaseSDK` class, which can be extended to create\nstructured and organized API SDKs for specific services. This promotes\ncode reusability and maintainability.\n\n```typescript\nimport {\n  ApiResponse,\n  BodyOptions,\n  createNetworkClient,\n  HttpMethod,\n  RequestOptions,\n  type NetworkClient,\n  type NetworkClientConfig,\n  BaseSDK // Import BaseSDK\n} from '@asaidimu/network-client';\n\ninterface Product {\n  id: string;\n  name: string;\n  price: number;\n}\n\n// Extend BaseSDK for your specific API\nclass ProductsAPI extends BaseSDK {\n  constructor(apiKey: string) {\n    super({\n      baseUrl: 'https://api.my-ecommerce.com/products',\n      defaultHeaders: {\n        'X-API-Key': apiKey,\n        'Content-Type': 'application/json',\n      },\n      defaultTimeout: 15000,\n    });\n  }\n\n  // Define specific methods for your API endpoints\n  async getProducts(): Promise<ApiResponse<Product[]>> {\n    return this.get<Product[]>('/');\n  }\n\n  async getProductById(id: string): Promise<ApiResponse<Product>> {\n    return this.get<Product>(`/${id}`);\n  }\n\n  async createProduct(productData: Omit<Product, 'id'>): Promise<ApiResponse<Product>> {\n    return this.post<Product>('/', productData);\n  }\n\n  async updateProduct(id: string, productData: Partial<Product>): Promise<ApiResponse<Product>> {\n    return this.put<Product>(`/${id}`, productData);\n  }\n\n  async uploadProductImage(productId: string, file: File): Promise<ApiResponse<void>> {\n    const formData = new FormData();\n    formData.append('image', file);\n    return this.post<void>(`/${productId}/image`, formData, {}, { type: 'form' });\n  }\n}\n\n// Usage of the SDK\nconst myProductsApi = new ProductsAPI('YOUR_SUPER_SECRET_API_KEY');\n\nasync function manageProducts() {\n  // Fetch all products\n  const productsResponse = await myProductsApi.getProducts();\n  if (productsResponse.success) {\n    console.log('All Products:', productsResponse.data);\n  } else {\n    console.error('Failed to get products:', productsResponse.error?.message);\n  }\n\n  // Create a new product\n  const newProduct = { name: 'Wireless Headphones', price: 99.99 };\n  const createResponse = await myProductsApi.createProduct(newProduct);\n  if (createResponse.success) {\n    console.log('Created Product:', createResponse.data);\n  } else {\n    console.error('Failed to create product:', createResponse.error?.message);\n  }\n}\n\nmanageProducts();\n```\n\n### Extending Client Instances\n\nThe `extend` method allows you to create new client instances with merged\nconfigurations, useful for overriding default settings for specific parts\nof your application without affecting the original client.\n\n```typescript\nimport { createNetworkClient } from '@asaidimu/network-client';\n\nconst baseClient = createNetworkClient({\n  baseUrl: 'https://api.example.com',\n  defaultHeaders: { 'X-App-Version': '1.0' },\n  defaultTimeout: 5000,\n});\n\n// Create a new client instance for authentication, overriding base URL and adding specific headers\nconst authClient = baseClient.extend({\n  baseUrl: 'https://auth.example.com/api',\n  defaultHeaders: { 'X-Auth-Flow': 'true' },\n  middleware: [\n    {\n      onRequest: async (context) => {\n        console.log(`Auth Client Request to: ${context.url}`);\n        return context;\n      }\n    }\n  ]\n});\n\n// Original client unchanged\nawait baseClient.get('/users'); // Uses 'https://api.example.com'\n\n// Auth client uses its own configuration\nawait authClient.post('/login', { username: 'test' }); // Uses 'https://auth.example.com/api'\n```\n\n---\n\n## 🚨 Error Handling\n\nThe client standardizes error handling through the `ApiResponse`\ninterface. When a request fails (e.g., network error, non-2xx HTTP\nstatus), the `success` property will be `false`, and the `error` property\nwill contain a `RequestError` object.\n\n```typescript\nimport { createNetworkClient, ApiResponse, RequestError, NetworkError } from '@asaidimu/network-client';\n\nconst client = createNetworkClient({ baseUrl: 'https://api.example.com' });\n\nasync function demonstrateErrorHandling() {\n  // Example 1: Non-existent endpoint (404 Not Found)\n  console.log('Attempting to access non-existent endpoint...');\n  const notFoundResponse: ApiResponse<any> = await client.get('/non-existent-path');\n  if (!notFoundResponse.success) {\n    const error = notFoundResponse.error as RequestError;\n    console.error('Error 1: Not Found');\n    console.error(`  Message: ${error.message}`);\n    console.error(`  Status: ${error.status}`);\n    console.error(`  URL: ${error.url}`);\n  }\n\n  // Example 2: Endpoint returning an error message in JSON (e.g., 400 Bad Request)\n  console.log('\\nAttempting to send invalid data...');\n  const badRequestResponse: ApiResponse<any> = await client.post('/items', { invalid_field: 123 });\n  if (!badRequestResponse.success) {\n    const error = badRequestResponse.error as RequestError;\n    console.error('Error 2: Bad Request');\n    console.error(`  Message: ${error.message}`); // This will try to extract message from JSON body\n    console.error(`  Status: ${error.status}`);\n  }\n\n  // Example 3: Simulating a network error (e.g., server offline, CORS issue)\n  console.log('\\nSimulating network error...');\n  try {\n    // This will likely throw if the URL is unreachable or CORS blocks\n    const networkErrorResponse: ApiResponse<any> = await client.get('http://nonexistent.domain');\n    if (!networkErrorResponse.success) {\n      const error = networkErrorResponse.error as RequestError;\n      console.error('Error 3: Network Error (caught via ApiResponse.error)');\n      console.error(`  Message: ${error.message}`);\n      console.error(`  Status: ${error.status || 'N/A'}`); // Status might be 0 for network errors\n    }\n  } catch (rawError: any) {\n    // Top-level network errors (e.g., fetch itself failing) might be caught here\n    if (rawError instanceof NetworkError) {\n      console.error('Error 3: Network Error (caught via NetworkError instance)');\n      console.error(`  Message: ${rawError.message}`);\n      console.error(`  Status: ${rawError.status || 'N/A'}`);\n    } else {\n      console.error('Error 3: Other unexpected error:', rawError.message);\n    }\n  }\n}\n\ndemonstrateErrorHandling();\n```\n\n---\n\n## 💙 TypeScript Support\n\n`@asaidimu/network-client` is built from the ground up with TypeScript,\nensuring robust type safety across all configurations, methods, and\nresponse data. This minimizes common runtime errors and improves\ndevelopment velocity.\n\n```typescript\nimport { createNetworkClient, ApiResponse, RequestOptions, RequestError } from '@asaimu/network-client';\n\ninterface Todo {\n  userId: number;\n  id: number;\n  title: string;\n  completed: boolean;\n}\n\ninterface UserProfile {\n  id: number;\n  name: string;\n  email: string;\n  address: {\n    street: string;\n    city: string;\n  };\n}\n\nconst client = createNetworkClient({ baseUrl: 'https://jsonplaceholder.typicode.com' });\n\nasync function getAndCreateData() {\n  // GET a list of todos - response.data will be typed as Todo[]\n  const todosResponse: ApiResponse<Todo[]> = await client.get<Todo[]>('/todos');\n  if (todosResponse.success) {\n    // TypeScript knows todosResponse.data is Todo[] here\n    console.log('First todo:', todosResponse.data[0].title);\n    // todosResponse.data[0].nonExistentProperty; // This would cause a TypeScript error\n  }\n\n  // GET a single user profile - response.data will be typed as UserProfile\n  const userResponse: ApiResponse<UserProfile> = await client.get<UserProfile>('/users/1');\n  if (userResponse.success) {\n    console.log('User email:', userResponse.data?.email);\n    console.log('User city:', userResponse.data?.address.city);\n  } else {\n    // TypeScript knows userResponse.error is RequestError here\n    console.error('Failed to fetch user:', userResponse.error?.message);\n  }\n\n  // POST a new todo - request body and response are typed\n  const newTodoData = { title: 'Learn Network Client', completed: false, userId: 1 };\n  const createTodoResponse: ApiResponse<Todo> = await client.post<Todo>('/todos', newTodoData);\n  if (createTodoResponse.success) {\n    console.log('New todo ID:', createTodoResponse.data?.id);\n    // createTodoResponse.data?.status; // This would cause a TypeScript error, 'status' is not on Todo\n  }\n}\n\ngetAndCreateData();\n```\n\n---\n\n## 🏛️ Project Architecture\n\nThe library is structured to be modular and easy to understand, separating core client logic from utility functions and SDK-building abstractions.\n\n### Core Components\n\n*   **`createNetworkClient`**: The primary factory function that initializes and returns a `NetworkClient` instance.\n*   **`NetworkClient` Interface**: Defines the public API for the client, including HTTP methods and utility functions.\n*   **`NetworkClientConfig`**: The configuration interface that allows extensive customization of client behavior.\n*   **`ApiResponse<T>`**: The standardized response wrapper that provides data, error, status, and raw response details.\n*   **`RequestError`**: Standardized error interface for network and API-level errors.\n*   **`Middleware`**: Interface for objects with `onRequest`, `onResponse`, and `onError` methods to extend client behavior.\n*   **`RequestInterceptor`, `ResponseInterceptor`**: Functional interfaces for modifying request and response contexts.\n*   **`SimpleCache`**: An internal utility class providing basic in-memory caching for GET requests.\n*   **`serializeBody`, `parseResponse`**: Internal utility functions for handling request body serialization and response data parsing.\n*   **`BaseSDK`**: An abstract class designed to be extended by developers to build their own typed API SDKs on top of the `NetworkClient`.\n\n### Data Flow\n\n1.  **Client Initialization**: `createNetworkClient` takes a `NetworkClientConfig` to set up defaults and behaviors.\n2.  **Request Creation**: A client method (e.g., `client.get`, `client.post`) is called, generating a `RequestContext`.\n3.  **Request Interception**: `RequestInterceptor` functions and `onRequest` middleware methods are executed, allowing modification of the `RequestContext` (e.g., adding headers, logging).\n4.  **Body Serialization**: The request body is serialized based on `BodyOptions` or auto-detection.\n5.  **Fetch Execution**: The `fetch` API is called with the prepared request.\n6.  **Response Handling**:\n    *   If the `fetch` call itself fails (e.g., network down, timeout), an `RequestError` is generated.\n    *   If `fetch` succeeds, the raw `Response` is received.\n    *   The `Response` is parsed based on `ResponseType` or auto-detection to extract data.\n7.  **Response Interception**: `ResponseInterceptor` functions and `onResponse` middleware methods are executed, allowing modification of the `ResponseContext` (e.g., data transformation, success logging).\n8.  **Error Interception**: If an error occurred (from fetch, parsing, or a non-2xx status), `onError` middleware methods are executed, allowing modification or logging of the `RequestError`.\n9.  **Retry Logic**: If configured, the `executeRequest` function might re-attempt the request based on the `retry` configuration and the nature of the error.\n10. **Cache Management**: For successful `GET` requests, data is stored in the `SimpleCache`. For subsequent `GET` requests, the cache is checked first.\n11. **Result Return**: An `ApiResponse<T>` object is returned to the caller, containing the parsed data or error, and request metadata.\n\n---\n\n## 🛠️ Development & Contributing\n\nWe welcome contributions from the community!\n\n### Development Setup\n\nTo set up the project for local development:\n\n1.  **Clone the repository:**\n    ```bash\n    git clone https://github.com/asaidimu/network-client.git\n    cd network-client\n    ```\n2.  **Install dependencies:**\n    This project uses `bun` for scripts, but `npm` or `yarn` will also work for dependency installation.\n    ```bash\n    bun install\n    # or\n    npm install\n    # or\n    yarn install\n    ```\n\n### Available Scripts\n\nThe `package.json` defines several scripts for common development tasks:\n\n*   `bun ci`: Installs dependencies. Used in CI environments.\n*   `bun clean`: Removes the `dist` directory.\n*   `bun prebuild`: Runs `bun clean` before `build`.\n*   `bun build`: Compiles TypeScript files into CommonJS (`.js`), ES Modules (`.mjs`), and TypeScript declaration files (`.d.ts`). Uses `tsup`.\n*   `bun postbuild`: Copies `README.md`, `LICENSE.md`, and `dist.package.json` into the `dist` directory, preparing for publishing.\n\nTo build the project:\n\n```bash\nbun build\n# or\nnpm run build\n```\n\n### Contributing Guidelines\n\nWe follow a standard GitHub pull request workflow:\n\n1.  **Fork** the repository.\n2.  **Clone** your forked repository.\n3.  **Create a new branch** for your feature or bug fix: `git checkout -b feature/my-new-feature` or `bugfix/fix-some-bug`.\n4.  **Make your changes**.\n5.  **Commit your changes** using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) (e.g., `feat: add new feature`, `fix: resolve bug`). This project uses `semantic-release`, which relies on these conventions.\n6.  **Push your branch** to your forked repository.\n7.  **Open a Pull Request** to the `main` branch of the original repository.\n8.  Ensure your code adheres to existing coding standards and passes linting/type checks.\n\n### Issue Reporting\n\nFound a bug or have a feature request? Please open an issue on our GitHub Issues page:\n[https://github.com/asaidimu/network-client/issues](https://github.com/asaidimu/network-client/issues)\n\nWhen reporting a bug, please include:\n\n*   A clear and concise description of the issue.\n*   Steps to reproduce the behavior.\n*   Expected behavior.\n*   Actual behavior.\n*   Any relevant error messages or console output.\n*   Your environment details (Node.js version, browser, OS).\n\n---\n\n## ℹ️ Additional Information\n\n### Troubleshooting\n\n*   **CORS Issues**: If you encounter \"Cross-Origin Request Blocked\"\n    errors, ensure your API server is configured to send appropriate CORS\n    headers (`Access-Control-Allow-Origin`,\n    `Access-Control-Allow-Methods`, `Access-Control-Allow-Headers`).\n*   **NetworkError with Status 0**: This often indicates a client-side\n    network issue (e.g., offline, DNS failure), or a CORS error where the\n    browser prevents access to the response.\n*   **Incorrect Body/Response Parsing**: Double-check `BodyOptions` and\n    `RequestOptions.responseType` to ensure they match the expected\n    content type of your API. The `auto` detection is smart but explicit\n    settings are safer for specific cases.\n*   **Timeout Not Working**: Ensure `timeout` is correctly set and\n    `AbortSignal` is not overriding it if passed directly. The client's\n    timeout mechanism relies on `AbortController`.\n\n### FAQ\n\n**Q: Can I use this client in Node.js?** \nA: While designed for browser environments and using `fetch`, Node.js v18+\nsupports the `fetch` API globally. Therefore, this client should work in\nNode.js environments as well.\n\n**Q: How do I handle file uploads?**\nA: Use `FormData` with `client.post` or `client.put`, and set `bodyOptions: { type: 'form' }`. The client will correctly send `multipart/form-data`.\n\n**Q: What if my API returns non-JSON errors?** \nA: The client attempts to parse non-2xx responses as JSON first. If that\nfails, it defaults to the raw text content of the error. You can also\nimplement `onError` middleware to parse specific error formats.\n\n**Q: How does `extend()` differ from just creating a new client?** \nA: `extend()` creates a new client instance inheriting the current\nclient's configuration, then applies the provided `newConfig` on top of\nit. This is useful for building a hierarchy of clients (e.g., a base\nclient, then an authenticated client derived from it). Creating a new\nclient starts from a blank slate with only the provided config.\n\n### Changelog / Roadmap\n\nFor a detailed history of changes and new features, please refer to the [CHANGELOG.md](CHANGELOG.md) file.\nThis project uses [semantic-release](https://semantic-release.gitbook.io/semantic-release/) for automated releases and changelog generation.\n\n### License\n\nThis project is licensed under the MIT License. See the [LICENSE.md](LICENSE.md) file for full details.\n\n### Acknowledgments\n\n*   Inspired by modern HTTP client patterns and the native Fetch API.\n*   Built with TypeScript for a superior developer experience.\n","readmeFilename":"README.md"}