@marianmeres/http-utils 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @marianmeres/http-utils
2
2
 
3
+ [![NPM version](https://img.shields.io/npm/v/@marianmeres/http-utils)](https://www.npmjs.com/package/@marianmeres/http-utils)
4
+ [![JSR version](https://jsr.io/badges/@marianmeres/http-utils)](https://jsr.io/@marianmeres/http-utils)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
3
7
  Opinionated, lightweight HTTP client wrapper for `fetch` with type-safe errors and convenient defaults.
4
8
 
5
9
  ## Features
@@ -64,186 +68,69 @@ try {
64
68
  }
65
69
  ```
66
70
 
67
- ## API Reference
71
+ ## API Overview
68
72
 
69
73
  ### `createHttpApi(base?, defaults?, errorExtractor?)`
70
74
 
71
75
  Creates an HTTP API client.
72
76
 
73
- **Parameters:**
74
- - `base` - Optional base URL for all requests
75
- - `defaults` - Optional default params (headers, credentials, etc.) or async function returning defaults
76
- - `errorExtractor` - Optional global error message extractor function
77
-
78
- **Returns:** Object with methods: `get`, `post`, `put`, `patch`, `del`, `url`, `base`
79
-
80
- ### HTTP Methods
81
-
82
- All methods return the parsed response body (JSON if possible) or throw `HttpError` on failure.
83
-
84
- **New Options API (recommended):**
85
77
  ```ts
86
- // GET with options
87
- await api.get(path, {
88
- params?: { headers?, signal?, credentials?, raw?, assert?, token? },
89
- respHeaders?: {},
90
- errorExtractor?: (body, response) => string
91
- });
92
-
93
- // POST/PUT/PATCH/DELETE with options
94
- await api.post(path, {
95
- data?: any, // Request body
96
- params?: { headers?, signal?, credentials?, raw?, assert?, token? },
97
- respHeaders?: {},
98
- errorExtractor?: (body, response) => string
78
+ const api = createHttpApi("https://api.example.com", {
79
+ headers: { "Authorization": "Bearer token" }
99
80
  });
100
81
  ```
101
82
 
102
- **Legacy API (still supported):**
103
- ```ts
104
- // GET
105
- await api.get(path, params?, respHeaders?, errorExtractor?)
106
-
107
- // POST, PUT, PATCH, DELETE
108
- await api.post(path, data?, params?, respHeaders?, errorExtractor?)
109
- ```
110
-
111
- **Common params:**
112
- - `headers` - Custom headers object
113
- - `token` - Bearer token (auto-adds `Authorization: Bearer {token}`)
114
- - `signal` - AbortSignal for cancellation
115
- - `credentials` - `'omit' | 'same-origin' | 'include'`
116
- - `raw` - Return raw Response object instead of parsed body
117
- - `assert` - Set to `false` to disable throwing on errors
118
-
119
- ### Response Headers
120
-
121
- Access response headers by passing a respHeaders object:
122
-
123
- ```ts
124
- // New API
125
- const headers = {};
126
- const data = await api.get("/users", { respHeaders: headers });
127
-
128
- console.log(headers.__http_status_code__); // 200
129
- console.log(headers["content-type"]); // "application/json"
130
-
131
- // Legacy API
132
- const legacyHeaders = {};
133
- const data2 = await api.get("/users", {}, legacyHeaders);
134
- ```
135
-
136
- ### Error Classes
137
-
138
- Well-known HTTP errors have specific classes:
139
-
140
- **Client Errors (4xx):**
141
- - `BadRequest` (400)
142
- - `Unauthorized` (401)
143
- - `Forbidden` (403)
144
- - `NotFound` (404)
145
- - `MethodNotAllowed` (405)
146
- - `RequestTimeout` (408)
147
- - `Conflict` (409)
148
- - `Gone` (410)
149
- - `LengthRequired` (411)
150
- - `ImATeapot` (418)
151
- - `UnprocessableContent` (422)
152
- - `TooManyRequests` (429)
153
-
154
- **Server Errors (5xx):**
155
- - `InternalServerError` (500)
156
- - `NotImplemented` (501)
157
- - `BadGateway` (502)
158
- - `ServiceUnavailable` (503)
159
-
160
- All errors extend `HttpError` with properties:
161
- - `status` - HTTP status code
162
- - `statusText` - HTTP status text
163
- - `body` - Response body (auto-parsed as JSON if possible)
164
- - `cause` - Error details/context
165
-
166
- ### HTTP Status Codes
167
-
168
- Access status codes via `HTTP_STATUS`:
169
-
170
- ```ts
171
- import { HTTP_STATUS } from "@marianmeres/http-utils";
172
-
173
- // By category
174
- HTTP_STATUS.SUCCESS.OK.CODE // 200
175
- HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.CODE // 404
176
-
177
- // Direct shortcuts
178
- HTTP_STATUS.OK // 200
179
- HTTP_STATUS.NOT_FOUND // 404
180
- HTTP_STATUS.INTERNAL_SERVER_ERROR // 500
181
-
182
- // Lookup by code
183
- const info = HTTP_STATUS.findByCode(404);
184
- // { CODE: 404, TEXT: "Not Found", _TYPE: "ERROR_CLIENT", _KEY: "NOT_FOUND" }
185
- ```
186
-
187
- ## Advanced Usage
188
-
189
- ### Error Message Extraction
190
-
191
- Customize how error messages are extracted from failed responses:
192
-
193
- ```ts
194
- // Global default
195
- createHttpApi.defaultErrorMessageExtractor = (body, response) => {
196
- return body.error?.message || response.statusText;
197
- };
198
-
199
- // Per-instance
200
- const api = createHttpApi(null, null, (body) => body.customError);
201
-
202
- // Per-request
203
- await api.get("/path", null, null, (body) => body.message);
204
- ```
205
-
206
- Priority: per-request → per-instance → global → built-in fallback
207
-
208
- ### Dynamic Configuration
83
+ ### HTTP Methods
209
84
 
210
85
  ```ts
211
- const api = createHttpApi("https://api.example.com", async () => {
212
- const token = await getToken(); // Fetch fresh token
213
- return { headers: { "Authorization": `Bearer ${token}` } };
86
+ // GET (new options API)
87
+ const data = await api.get("/users", {
88
+ params: { headers: { "X-Custom": "value" } },
89
+ respHeaders: {}
214
90
  });
215
- ```
216
91
 
217
- ### Raw Response Access
92
+ // POST/PUT/PATCH/DELETE (new options API)
93
+ await api.post("/users", {
94
+ data: { name: "John" },
95
+ params: { token: "bearer-token" }
96
+ });
218
97
 
219
- ```ts
220
- const response = await api.get("/users", { raw: true });
221
- console.log(response instanceof Response); // true
222
- const data = await response.json();
98
+ // Legacy API still supported
99
+ const data = await api.get("/users", { headers: { "X-Custom": "value" } });
100
+ await api.post("/users", { name: "John" });
223
101
  ```
224
102
 
225
- ### Non-Throwing Errors
103
+ ### Error Handling
226
104
 
227
105
  ```ts
228
- const data = await api.get("/might-fail", { assert: false });
229
- if (data.error) {
230
- console.log("Request failed:", data.error.message);
106
+ import { HTTP_ERROR, NotFound } from "@marianmeres/http-utils";
107
+
108
+ try {
109
+ await api.get("/resource");
110
+ } catch (error) {
111
+ if (error instanceof NotFound) {
112
+ console.log("Not found:", error.body);
113
+ }
114
+ // All errors have: status, statusText, body, cause
231
115
  }
232
116
  ```
233
117
 
234
- ### AbortController Support
118
+ ### Key Features
235
119
 
236
- ```ts
237
- const controller = new AbortController();
120
+ - **Auto JSON**: Response bodies are automatically parsed as JSON
121
+ - **Bearer tokens**: Use `token` param to auto-add `Authorization: Bearer` header
122
+ - **Response headers**: Pass `respHeaders: {}` to capture response headers
123
+ - **Raw response**: Use `raw: true` to get the raw Response object
124
+ - **Non-throwing**: Use `assert: false` to prevent throwing on errors
125
+ - **AbortController**: Pass `signal` for request cancellation
238
126
 
239
- setTimeout(() => controller.abort(), 5000);
127
+ ## Full API Reference
240
128
 
241
- await api.get("/slow-endpoint", { signal: controller.signal });
242
- ```
129
+ For complete API documentation including all error classes, HTTP status codes, types, and utilities, see **[API.md](API.md)**.
243
130
 
244
131
  ## Utilities
245
132
 
246
- ### `getErrorMessage(error, stripErrorPrefix?)`
133
+ ### `getErrorMessage(error)`
247
134
 
248
135
  Extracts human-readable messages from any error format:
249
136
 
@@ -265,9 +152,5 @@ Manually create HTTP errors:
265
152
  import { createHttpError } from "@marianmeres/http-utils";
266
153
 
267
154
  const error = createHttpError(404, "User not found", { userId: 123 });
268
- throw error;
269
- ```
270
-
271
- ## License
272
-
273
- MIT
155
+ throw error; // instanceof NotFound
156
+ ```
package/dist/api.d.ts CHANGED
@@ -1,34 +1,74 @@
1
+ /**
2
+ * @module api
3
+ *
4
+ * HTTP API client factory and related types.
5
+ * Provides a convenient wrapper over the native `fetch` API with sensible defaults.
6
+ */
7
+ /**
8
+ * Request body data type.
9
+ * Supports JSON-serializable objects, FormData for file uploads, or raw strings.
10
+ */
11
+ export type RequestData = Record<string, unknown> | FormData | string | null;
1
12
  interface BaseParams {
2
13
  method: 'GET' | 'POST' | 'PATCH' | 'DELETE' | 'PUT';
3
14
  path: string;
4
15
  }
5
- interface FetchParams {
6
- data?: any;
16
+ /**
17
+ * Parameters for fetch requests.
18
+ */
19
+ export interface FetchParams {
20
+ /** Request body data (automatically JSON stringified unless FormData). */
21
+ data?: RequestData;
22
+ /** Bearer token (auto-adds `Authorization: Bearer {token}` header). */
7
23
  token?: string | null;
24
+ /** Custom request headers. */
8
25
  headers?: Record<string, string> | null;
26
+ /** AbortSignal for request cancellation. */
9
27
  signal?: AbortSignal;
28
+ /** Credentials mode for the request. */
10
29
  credentials?: 'omit' | 'same-origin' | 'include' | null;
30
+ /** If true, returns the raw Response object instead of parsed body. */
11
31
  raw?: boolean | null;
32
+ /** If false, does not throw on HTTP errors (default: true). */
12
33
  assert?: boolean | null;
13
34
  }
14
35
  type BaseFetchParams = BaseParams & FetchParams;
15
- type ErrorMessageExtractor = (body: any, response: Response) => string;
16
- type ResponseHeaders = Record<string, string | number>;
17
36
  /**
18
- * Options for HTTP GET requests (new cleaner API).
37
+ * Function to extract error messages from failed HTTP responses.
38
+ * @param body - The parsed response body.
39
+ * @param response - The raw Response object.
40
+ * @returns A human-readable error message string.
41
+ */
42
+ export type ErrorMessageExtractor = (body: unknown, response: Response) => string;
43
+ /**
44
+ * Object to receive response headers after a request completes.
45
+ * Will be mutated to include all response headers plus special keys:
46
+ * - `__http_status_code__`: The HTTP status code
47
+ * - `__http_status_text__`: The HTTP status text
48
+ */
49
+ export type ResponseHeaders = Record<string, string | number>;
50
+ /**
51
+ * Options for HTTP GET requests using the new cleaner API.
19
52
  */
20
53
  export interface GetOptions {
54
+ /** Fetch parameters (headers, token, signal, credentials, raw, assert). */
21
55
  params?: FetchParams;
56
+ /** Object to receive response headers (will be mutated). */
22
57
  respHeaders?: ResponseHeaders | null;
58
+ /** Custom error message extractor for this request. */
23
59
  errorExtractor?: ErrorMessageExtractor | null;
24
60
  }
25
61
  /**
26
- * Options for HTTP POST/PUT/PATCH/DELETE requests (new cleaner API).
62
+ * Options for HTTP POST/PUT/PATCH/DELETE requests using the new cleaner API.
27
63
  */
28
64
  export interface DataOptions {
29
- data?: any;
65
+ /** Request body data. */
66
+ data?: RequestData;
67
+ /** Fetch parameters (headers, token, signal, credentials, raw, assert). */
30
68
  params?: FetchParams;
69
+ /** Object to receive response headers (will be mutated). */
31
70
  respHeaders?: ResponseHeaders | null;
71
+ /** Custom error message extractor for this request. */
32
72
  errorExtractor?: ErrorMessageExtractor | null;
33
73
  }
34
74
  /**
@@ -53,7 +93,7 @@ export declare class HttpApi {
53
93
  * });
54
94
  * ```
55
95
  */
56
- get(path: string, options: GetOptions): Promise<any>;
96
+ get(path: string, options: GetOptions): Promise<unknown>;
57
97
  /**
58
98
  * Performs a GET request (legacy API).
59
99
  *
@@ -65,7 +105,7 @@ export declare class HttpApi {
65
105
  * @returns The response body (auto-parsed as JSON if possible), or Response if `raw: true`.
66
106
  * @throws {HttpError} When the response is not OK and `assert` is true (default).
67
107
  */
68
- get(path: string, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<any>;
108
+ get(path: string, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<unknown>;
69
109
  /**
70
110
  * Performs a POST request (new options API - recommended).
71
111
  *
@@ -83,7 +123,7 @@ export declare class HttpApi {
83
123
  * });
84
124
  * ```
85
125
  */
86
- post(path: string, options: DataOptions): Promise<any>;
126
+ post(path: string, options: DataOptions): Promise<unknown>;
87
127
  /**
88
128
  * Performs a POST request (legacy API).
89
129
  *
@@ -96,23 +136,23 @@ export declare class HttpApi {
96
136
  * @returns The response body (auto-parsed as JSON if possible), or Response if `raw: true`.
97
137
  * @throws {HttpError} When the response is not OK and `assert` is true (default).
98
138
  */
99
- post(path: string, data?: any, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<any>;
139
+ post(path: string, data?: RequestData, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<unknown>;
100
140
  /** Performs a PUT request (new options API). @see post */
101
- put(path: string, options: DataOptions): Promise<any>;
141
+ put(path: string, options: DataOptions): Promise<unknown>;
102
142
  /** Performs a PUT request (legacy API). @see post */
103
- put(path: string, data?: any, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<any>;
143
+ put(path: string, data?: RequestData, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<unknown>;
104
144
  /** Performs a PATCH request (new options API). @see post */
105
- patch(path: string, options: DataOptions): Promise<any>;
145
+ patch(path: string, options: DataOptions): Promise<unknown>;
106
146
  /** Performs a PATCH request (legacy API). @see post */
107
- patch(path: string, data?: any, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<any>;
147
+ patch(path: string, data?: RequestData, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<unknown>;
108
148
  /**
109
149
  * Performs a DELETE request (new options API).
110
150
  * Note: Request body in DELETE is allowed per HTTP spec.
111
151
  * @see post
112
152
  */
113
- del(path: string, options: DataOptions): Promise<any>;
153
+ del(path: string, options: DataOptions): Promise<unknown>;
114
154
  /** Performs a DELETE request (legacy API). @see post */
115
- del(path: string, data?: any, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<any>;
155
+ del(path: string, data?: RequestData, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<unknown>;
116
156
  /**
117
157
  * Helper method to build the full URL from a path.
118
158
  *
package/dist/api.js CHANGED
@@ -1,6 +1,10 @@
1
+ /**
2
+ * @module api
3
+ *
4
+ * HTTP API client factory and related types.
5
+ * Provides a convenient wrapper over the native `fetch` API with sensible defaults.
6
+ */
1
7
  import { createHttpError } from './error.js';
2
- // This is an opinionated HTTP client wrapper and may not be suitable for every use case.
3
- // It provides convenient defaults over plain fetch calls without adding unnecessary abstractions.
4
8
  /**
5
9
  * Deep merges two objects. Later properties overwrite earlier properties.
6
10
  */
@@ -8,23 +12,25 @@ function deepMerge(target, source) {
8
12
  const output = { ...target };
9
13
  if (isObject(target) && isObject(source)) {
10
14
  Object.keys(source).forEach(key => {
11
- if (isObject(source[key])) {
12
- if (!(key in target)) {
13
- Object.assign(output, { [key]: source[key] });
15
+ const sourceVal = source[key];
16
+ const targetVal = target[key];
17
+ if (isObject(sourceVal)) {
18
+ if (!(key in target) || !isObject(targetVal)) {
19
+ Object.assign(output, { [key]: sourceVal });
14
20
  }
15
21
  else {
16
- output[key] = deepMerge(target[key], source[key]);
22
+ output[key] = deepMerge(targetVal, sourceVal);
17
23
  }
18
24
  }
19
25
  else {
20
- Object.assign(output, { [key]: source[key] });
26
+ Object.assign(output, { [key]: sourceVal });
21
27
  }
22
28
  });
23
29
  }
24
30
  return output;
25
31
  }
26
32
  function isObject(item) {
27
- return item && typeof item === 'object' && !Array.isArray(item);
33
+ return item !== null && typeof item === 'object' && !Array.isArray(item);
28
34
  }
29
35
  const _fetchRaw = async ({ method, path, data = null, token = null, headers = null, signal, credentials, }) => {
30
36
  const normalizedHeaders = Object.entries(headers || {}).reduce((m, [k, v]) => ({ ...m, [k.toLowerCase()]: v }), {});
@@ -84,13 +90,14 @@ const _fetch = async (params, respHeaders = null, errorMessageExtractor = null,
84
90
  createHttpApi.defaultErrorMessageExtractor ?? // static default
85
91
  // educated guess fallback
86
92
  function (_body, _response) {
87
- let msg =
93
+ const b = _body;
94
+ let msg = String(
88
95
  // try opinionated convention first
89
- _body?.error?.message ||
90
- _body?.message ||
91
- _body?.error ||
96
+ b?.error?.message ||
97
+ b?.message ||
98
+ b?.error ||
92
99
  _response?.statusText ||
93
- 'Unknown error';
100
+ 'Unknown error');
94
101
  if (msg.length > 255)
95
102
  msg = `[Shortened]: ${msg.slice(0, 255)}`;
96
103
  return msg;
@@ -161,10 +168,13 @@ export class HttpApi {
161
168
  let fetchParams;
162
169
  let headers = null;
163
170
  let extractor = null;
164
- if (dataOrOptions && ('data' in dataOrOptions ||
165
- 'params' in dataOrOptions ||
166
- 'respHeaders' in dataOrOptions ||
167
- 'errorExtractor' in dataOrOptions)) {
171
+ if (dataOrOptions &&
172
+ typeof dataOrOptions === 'object' &&
173
+ !(dataOrOptions instanceof FormData) &&
174
+ ('data' in dataOrOptions ||
175
+ 'params' in dataOrOptions ||
176
+ 'respHeaders' in dataOrOptions ||
177
+ 'errorExtractor' in dataOrOptions)) {
168
178
  // New options API
169
179
  const opts = dataOrOptions;
170
180
  data = opts.data ?? null;
@@ -187,10 +197,13 @@ export class HttpApi {
187
197
  let fetchParams;
188
198
  let headers = null;
189
199
  let extractor = null;
190
- if (dataOrOptions && ('data' in dataOrOptions ||
191
- 'params' in dataOrOptions ||
192
- 'respHeaders' in dataOrOptions ||
193
- 'errorExtractor' in dataOrOptions)) {
200
+ if (dataOrOptions &&
201
+ typeof dataOrOptions === 'object' &&
202
+ !(dataOrOptions instanceof FormData) &&
203
+ ('data' in dataOrOptions ||
204
+ 'params' in dataOrOptions ||
205
+ 'respHeaders' in dataOrOptions ||
206
+ 'errorExtractor' in dataOrOptions)) {
194
207
  const opts = dataOrOptions;
195
208
  data = opts.data ?? null;
196
209
  fetchParams = opts.params;
@@ -211,10 +224,13 @@ export class HttpApi {
211
224
  let fetchParams;
212
225
  let headers = null;
213
226
  let extractor = null;
214
- if (dataOrOptions && ('data' in dataOrOptions ||
215
- 'params' in dataOrOptions ||
216
- 'respHeaders' in dataOrOptions ||
217
- 'errorExtractor' in dataOrOptions)) {
227
+ if (dataOrOptions &&
228
+ typeof dataOrOptions === 'object' &&
229
+ !(dataOrOptions instanceof FormData) &&
230
+ ('data' in dataOrOptions ||
231
+ 'params' in dataOrOptions ||
232
+ 'respHeaders' in dataOrOptions ||
233
+ 'errorExtractor' in dataOrOptions)) {
218
234
  const opts = dataOrOptions;
219
235
  data = opts.data ?? null;
220
236
  fetchParams = opts.params;
@@ -235,10 +251,13 @@ export class HttpApi {
235
251
  let fetchParams;
236
252
  let headers = null;
237
253
  let extractor = null;
238
- if (dataOrOptions && ('data' in dataOrOptions ||
239
- 'params' in dataOrOptions ||
240
- 'respHeaders' in dataOrOptions ||
241
- 'errorExtractor' in dataOrOptions)) {
254
+ if (dataOrOptions &&
255
+ typeof dataOrOptions === 'object' &&
256
+ !(dataOrOptions instanceof FormData) &&
257
+ ('data' in dataOrOptions ||
258
+ 'params' in dataOrOptions ||
259
+ 'respHeaders' in dataOrOptions ||
260
+ 'errorExtractor' in dataOrOptions)) {
242
261
  const opts = dataOrOptions;
243
262
  data = opts.data ?? null;
244
263
  fetchParams = opts.params;
@@ -295,4 +314,16 @@ export class HttpApi {
295
314
  export function createHttpApi(base, defaults, factoryErrorMessageExtractor) {
296
315
  return new HttpApi(base, defaults, factoryErrorMessageExtractor);
297
316
  }
317
+ /**
318
+ * Global default error message extractor.
319
+ * Applied to all requests unless overridden at instance or request level.
320
+ * Priority: per-request → per-instance → global → built-in fallback.
321
+ *
322
+ * @example
323
+ * ```ts
324
+ * createHttpApi.defaultErrorMessageExtractor = (body, response) => {
325
+ * return body?.error?.message || response.statusText;
326
+ * };
327
+ * ```
328
+ */
298
329
  createHttpApi.defaultErrorMessageExtractor = null;