@marianmeres/http-utils 2.0.2 → 2.2.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/AGENTS.md ADDED
@@ -0,0 +1,197 @@
1
+ # AGENTS.md - Machine-Readable Package Context
2
+
3
+ ## Package Identity
4
+
5
+ ```yaml
6
+ name: "@marianmeres/http-utils"
7
+ version: "2.0.2"
8
+ license: MIT
9
+ runtime: deno, node
10
+ type: library
11
+ category: http-client
12
+ ```
13
+
14
+ ## Purpose
15
+
16
+ Lightweight, opinionated HTTP client wrapper for the native `fetch` API. Provides type-safe HTTP errors mapped to specific error classes (e.g., 404 → NotFound), convenient defaults (auto JSON parsing, Bearer token support, base URLs), and flexible three-tier error message extraction.
17
+
18
+ ## Architecture
19
+
20
+ ```
21
+ src/
22
+ ├── mod.ts # Public exports (main entry point)
23
+ ├── api.ts # HttpApi class, createHttpApi factory
24
+ ├── error.ts # HttpError classes, HTTP_ERROR namespace
25
+ └── status.ts # HTTP_STATUS codes and lookup
26
+ ```
27
+
28
+ ## Public API
29
+
30
+ ### Primary Export: createHttpApi
31
+
32
+ ```typescript
33
+ function createHttpApi(
34
+ base?: string | null,
35
+ defaults?: Partial<FetchParams> | (() => Promise<Partial<FetchParams>>),
36
+ factoryErrorMessageExtractor?: ErrorMessageExtractor | null
37
+ ): HttpApi
38
+ ```
39
+
40
+ ### HttpApi Methods
41
+
42
+ | Method | Signature | Description |
43
+ |--------|-----------|-------------|
44
+ | `get` | `get(path, options?: GetOptions): Promise<unknown>` | GET request |
45
+ | `post` | `post(path, options?: DataOptions): Promise<unknown>` | POST request |
46
+ | `put` | `put(path, options?: DataOptions): Promise<unknown>` | PUT request |
47
+ | `patch` | `patch(path, options?: DataOptions): Promise<unknown>` | PATCH request |
48
+ | `del` | `del(path, options?: DataOptions): Promise<unknown>` | DELETE request |
49
+ | `url` | `url(path: string): string` | Build full URL |
50
+ | `base` | `get/set base: string \| null` | Base URL property |
51
+
52
+ ### Exported Types
53
+
54
+ ```typescript
55
+ type RequestData = Record<string, unknown> | FormData | string | null;
56
+
57
+ interface FetchParams {
58
+ data?: RequestData;
59
+ token?: string | null;
60
+ headers?: Record<string, string> | null;
61
+ signal?: AbortSignal;
62
+ credentials?: 'omit' | 'same-origin' | 'include' | null;
63
+ raw?: boolean | null;
64
+ assert?: boolean | null;
65
+ }
66
+
67
+ interface GetOptions {
68
+ params?: FetchParams;
69
+ respHeaders?: ResponseHeaders | null;
70
+ errorExtractor?: ErrorMessageExtractor | null;
71
+ }
72
+
73
+ interface DataOptions {
74
+ data?: RequestData;
75
+ params?: FetchParams;
76
+ respHeaders?: ResponseHeaders | null;
77
+ errorExtractor?: ErrorMessageExtractor | null;
78
+ }
79
+
80
+ type ErrorMessageExtractor = (body: unknown, response: Response) => string;
81
+ type ResponseHeaders = Record<string, string | number>;
82
+ ```
83
+
84
+ ### Error Classes (HTTP_ERROR namespace)
85
+
86
+ ```typescript
87
+ HTTP_ERROR.HttpError // Base class (default 500)
88
+ HTTP_ERROR.BadRequest // 400
89
+ HTTP_ERROR.Unauthorized // 401
90
+ HTTP_ERROR.Forbidden // 403
91
+ HTTP_ERROR.NotFound // 404
92
+ HTTP_ERROR.MethodNotAllowed // 405
93
+ HTTP_ERROR.RequestTimeout // 408
94
+ HTTP_ERROR.Conflict // 409
95
+ HTTP_ERROR.Gone // 410
96
+ HTTP_ERROR.LengthRequired // 411
97
+ HTTP_ERROR.ImATeapot // 418
98
+ HTTP_ERROR.UnprocessableContent // 422
99
+ HTTP_ERROR.TooManyRequests // 429
100
+ HTTP_ERROR.InternalServerError // 500
101
+ HTTP_ERROR.NotImplemented // 501
102
+ HTTP_ERROR.BadGateway // 502
103
+ HTTP_ERROR.ServiceUnavailable // 503
104
+ ```
105
+
106
+ ### Utility Functions
107
+
108
+ ```typescript
109
+ function createHttpError(code: number | string, message?: string | null, body?: unknown, cause?: unknown): HttpError
110
+ function getErrorMessage(e: unknown, stripErrorPrefix?: boolean): string
111
+ ```
112
+
113
+ ### HTTP Status Codes
114
+
115
+ ```typescript
116
+ class HTTP_STATUS {
117
+ static readonly INFO: {...} // 1xx
118
+ static readonly SUCCESS: {...} // 2xx
119
+ static readonly REDIRECT: {...} // 3xx
120
+ static readonly ERROR_CLIENT: {...} // 4xx
121
+ static readonly ERROR_SERVER: {...} // 5xx
122
+
123
+ // Direct shortcuts
124
+ static readonly OK: 200
125
+ static readonly NOT_FOUND: 404
126
+ // ... etc
127
+
128
+ static findByCode(code: number | string): { CODE, TEXT, _TYPE, _KEY } | null
129
+ }
130
+ ```
131
+
132
+ ## Key Behaviors
133
+
134
+ 1. **Auto JSON parsing**: Response bodies are automatically parsed as JSON if possible
135
+ 2. **Bearer token**: `token` param auto-adds `Authorization: Bearer {token}` header
136
+ 3. **Error throwing**: By default, non-OK responses throw HttpError (disable with `assert: false`)
137
+ 4. **Response headers**: Pass `respHeaders: {}` to capture response headers (mutated in place)
138
+ 5. **Raw response**: Use `raw: true` to get raw Response object instead of parsed body
139
+ 6. **Error priority**: per-request extractor → per-instance → global → built-in fallback
140
+
141
+ ## Development Commands
142
+
143
+ ```bash
144
+ deno task test # Run tests
145
+ deno task test:watch # Run tests in watch mode
146
+ deno task npm:build # Build for NPM
147
+ deno task publish # Publish to JSR and NPM
148
+ ```
149
+
150
+ ## Dependencies
151
+
152
+ - **Runtime**: None (zero dependencies)
153
+ - **Dev**: @std/assert (testing)
154
+
155
+ ## File Locations
156
+
157
+ | Purpose | Path |
158
+ |---------|------|
159
+ | Entry point | `src/mod.ts` |
160
+ | HttpApi implementation | `src/api.ts` |
161
+ | Error classes | `src/error.ts` |
162
+ | Status codes | `src/status.ts` |
163
+ | Tests | `tests/*.test.ts` |
164
+ | NPM output | `.npm-dist/` |
165
+
166
+ ## Common Patterns
167
+
168
+ ### Basic Usage
169
+
170
+ ```typescript
171
+ const api = createHttpApi("https://api.example.com", {
172
+ headers: { "Authorization": "Bearer token" }
173
+ });
174
+
175
+ const data = await api.get("/users");
176
+ await api.post("/users", { data: { name: "John" } });
177
+ ```
178
+
179
+ ### Error Handling
180
+
181
+ ```typescript
182
+ try {
183
+ await api.get("/resource");
184
+ } catch (error) {
185
+ if (error instanceof HTTP_ERROR.NotFound) {
186
+ // Handle 404
187
+ }
188
+ }
189
+ ```
190
+
191
+ ### Dynamic Token
192
+
193
+ ```typescript
194
+ const api = createHttpApi("https://api.example.com", async () => ({
195
+ headers: { "Authorization": `Bearer ${await getToken()}` }
196
+ }));
197
+ ```
package/API.md ADDED
@@ -0,0 +1,464 @@
1
+ # API Documentation
2
+
3
+ Complete API reference for `@marianmeres/http-utils`.
4
+
5
+ ## Table of Contents
6
+
7
+ - [createHttpApi](#createhttpapi)
8
+ - [HttpApi Class](#httpapi-class)
9
+ - [Types](#types)
10
+ - [HTTP Errors](#http-errors)
11
+ - [HTTP Status Codes](#http-status-codes)
12
+ - [Utilities](#utilities)
13
+
14
+ ---
15
+
16
+ ## createHttpApi
17
+
18
+ Creates an HTTP API client with convenient defaults and error handling.
19
+
20
+ ```ts
21
+ function createHttpApi(
22
+ base?: string | null,
23
+ defaults?: Partial<FetchParams> | (() => Promise<Partial<FetchParams>>),
24
+ factoryErrorMessageExtractor?: ErrorMessageExtractor | null
25
+ ): HttpApi
26
+ ```
27
+
28
+ ### Parameters
29
+
30
+ | Parameter | Type | Description |
31
+ |-----------|------|-------------|
32
+ | `base` | `string \| null` | Optional base URL to prepend to all requests. |
33
+ | `defaults` | `object \| function` | Optional default parameters or async function returning defaults. |
34
+ | `factoryErrorMessageExtractor` | `ErrorMessageExtractor \| null` | Optional function to extract error messages from failed responses. |
35
+
36
+ ### Returns
37
+
38
+ An `HttpApi` instance with methods: `get`, `post`, `put`, `patch`, `del`, `url`, and `base` property.
39
+
40
+ ### Example
41
+
42
+ ```ts
43
+ import { createHttpApi } from "@marianmeres/http-utils";
44
+
45
+ // Basic usage
46
+ const api = createHttpApi("https://api.example.com");
47
+
48
+ // With default headers
49
+ const api = createHttpApi("https://api.example.com", {
50
+ headers: { "Authorization": "Bearer token" }
51
+ });
52
+
53
+ // With dynamic defaults (e.g., for token refresh)
54
+ const api = createHttpApi("https://api.example.com", async () => {
55
+ const token = await getToken();
56
+ return { headers: { "Authorization": `Bearer ${token}` } };
57
+ });
58
+
59
+ // With custom error extractor
60
+ const api = createHttpApi("https://api.example.com", null, (body) => {
61
+ return body?.error?.message || "Unknown error";
62
+ });
63
+ ```
64
+
65
+ ### Static Properties
66
+
67
+ #### `createHttpApi.defaultErrorMessageExtractor`
68
+
69
+ Global default error message extractor. Applied to all requests unless overridden.
70
+
71
+ ```ts
72
+ createHttpApi.defaultErrorMessageExtractor = (body, response) => {
73
+ return body?.error?.message || response.statusText;
74
+ };
75
+ ```
76
+
77
+ Priority order: per-request > per-instance > global > built-in fallback.
78
+
79
+ ---
80
+
81
+ ## HttpApi Class
82
+
83
+ HTTP API client class. Usually created via `createHttpApi()`.
84
+
85
+ ### Methods
86
+
87
+ #### `get<T>(path, options?)`
88
+
89
+ Performs a GET request.
90
+
91
+ **New Options API (recommended):**
92
+ ```ts
93
+ async get<T = unknown>(path: string, options?: GetOptions): Promise<T>
94
+ ```
95
+
96
+ **Legacy API:**
97
+ ```ts
98
+ async get<T = unknown>(
99
+ path: string,
100
+ params?: FetchParams,
101
+ respHeaders?: ResponseHeaders | null,
102
+ errorMessageExtractor?: ErrorMessageExtractor | null
103
+ ): Promise<T>
104
+ ```
105
+
106
+ **Example:**
107
+ ```ts
108
+ // New API with type parameter
109
+ interface User { id: number; name: string; }
110
+ const user = await api.get<User>("/users/1", {
111
+ params: { headers: { "X-Custom": "value" } },
112
+ respHeaders: {}
113
+ });
114
+
115
+ // Without type parameter (returns unknown)
116
+ const data = await api.get("/users");
117
+
118
+ // Legacy API
119
+ const data = await api.get("/users", { headers: { "X-Custom": "value" } });
120
+ ```
121
+
122
+ #### `post<T>(path, options?)`
123
+
124
+ Performs a POST request.
125
+
126
+ **New Options API (recommended):**
127
+ ```ts
128
+ async post<T = unknown>(path: string, options?: DataOptions): Promise<T>
129
+ ```
130
+
131
+ **Legacy API:**
132
+ ```ts
133
+ async post<T = unknown>(
134
+ path: string,
135
+ data?: RequestData,
136
+ params?: FetchParams,
137
+ respHeaders?: ResponseHeaders | null,
138
+ errorMessageExtractor?: ErrorMessageExtractor | null
139
+ ): Promise<T>
140
+ ```
141
+
142
+ **Example:**
143
+ ```ts
144
+ // New API with type parameter
145
+ interface User { id: number; name: string; }
146
+ const user = await api.post<User>("/users", {
147
+ data: { name: "John" },
148
+ params: { headers: { "X-Custom": "value" } }
149
+ });
150
+
151
+ // Legacy API
152
+ const result = await api.post("/users", { name: "John" });
153
+ ```
154
+
155
+ #### `put<T>(path, options?)`
156
+
157
+ Performs a PUT request. Same signature as `post<T>()`.
158
+
159
+ #### `patch<T>(path, options?)`
160
+
161
+ Performs a PATCH request. Same signature as `post<T>()`.
162
+
163
+ #### `del<T>(path, options?)`
164
+
165
+ Performs a DELETE request. Same signature as `post<T>()`.
166
+
167
+ #### `url(path)`
168
+
169
+ Builds the full URL from a path.
170
+
171
+ ```ts
172
+ url(path: string): string
173
+ ```
174
+
175
+ **Example:**
176
+ ```ts
177
+ const api = createHttpApi("https://api.example.com");
178
+ api.url("/users"); // "https://api.example.com/users"
179
+ api.url("https://other.com/path"); // "https://other.com/path" (absolute URLs returned as-is)
180
+ ```
181
+
182
+ ### Properties
183
+
184
+ #### `base`
185
+
186
+ Get or set the base URL.
187
+
188
+ ```ts
189
+ get base(): string | null | undefined
190
+ set base(v: string | null | undefined)
191
+ ```
192
+
193
+ ---
194
+
195
+ ## Types
196
+
197
+ ### RequestData
198
+
199
+ Request body data type.
200
+
201
+ ```ts
202
+ type RequestData = Record<string, unknown> | FormData | string | null;
203
+ ```
204
+
205
+ ### FetchParams
206
+
207
+ Parameters for fetch requests.
208
+
209
+ ```ts
210
+ interface FetchParams {
211
+ /** Request body data (automatically JSON stringified unless FormData). */
212
+ data?: RequestData;
213
+ /** Bearer token (auto-adds `Authorization: Bearer {token}` header). */
214
+ token?: string | null;
215
+ /** Custom request headers. */
216
+ headers?: Record<string, string> | null;
217
+ /** AbortSignal for request cancellation. */
218
+ signal?: AbortSignal;
219
+ /** Credentials mode for the request. */
220
+ credentials?: 'omit' | 'same-origin' | 'include' | null;
221
+ /** If true, returns the raw Response object instead of parsed body. */
222
+ raw?: boolean | null;
223
+ /** If false, does not throw on HTTP errors (default: true). */
224
+ assert?: boolean | null;
225
+ }
226
+ ```
227
+
228
+ ### GetOptions
229
+
230
+ Options for HTTP GET requests (new API).
231
+
232
+ ```ts
233
+ interface GetOptions {
234
+ /** Fetch parameters (headers, token, signal, credentials, raw, assert). */
235
+ params?: FetchParams;
236
+ /** Object to receive response headers (will be mutated). */
237
+ respHeaders?: ResponseHeaders | null;
238
+ /** Custom error message extractor for this request. */
239
+ errorExtractor?: ErrorMessageExtractor | null;
240
+ }
241
+ ```
242
+
243
+ ### DataOptions
244
+
245
+ Options for HTTP POST/PUT/PATCH/DELETE requests (new API).
246
+
247
+ ```ts
248
+ interface DataOptions {
249
+ /** Request body data. */
250
+ data?: RequestData;
251
+ /** Fetch parameters (headers, token, signal, credentials, raw, assert). */
252
+ params?: FetchParams;
253
+ /** Object to receive response headers (will be mutated). */
254
+ respHeaders?: ResponseHeaders | null;
255
+ /** Custom error message extractor for this request. */
256
+ errorExtractor?: ErrorMessageExtractor | null;
257
+ }
258
+ ```
259
+
260
+ ### ResponseHeaders
261
+
262
+ Object to receive response headers after a request completes.
263
+
264
+ ```ts
265
+ type ResponseHeaders = Record<string, string | number>;
266
+ ```
267
+
268
+ Special keys added after request:
269
+ - `__http_status_code__`: The HTTP status code
270
+ - `__http_status_text__`: The HTTP status text
271
+
272
+ ### ErrorMessageExtractor
273
+
274
+ Function to extract error messages from failed HTTP responses.
275
+
276
+ ```ts
277
+ type ErrorMessageExtractor = (body: unknown, response: Response) => string;
278
+ ```
279
+
280
+ ---
281
+
282
+ ## HTTP Errors
283
+
284
+ All errors extend `HttpError` base class.
285
+
286
+ ### HttpError (Base Class)
287
+
288
+ ```ts
289
+ class HttpError extends Error {
290
+ status: number; // HTTP status code
291
+ statusText: string; // HTTP status text
292
+ body: unknown; // Response body (auto-parsed as JSON)
293
+ cause: unknown; // Error cause/details
294
+ }
295
+ ```
296
+
297
+ ### Client Errors (4xx)
298
+
299
+ | Class | Status | Description |
300
+ |-------|--------|-------------|
301
+ | `BadRequest` | 400 | Bad Request |
302
+ | `Unauthorized` | 401 | Unauthorized |
303
+ | `Forbidden` | 403 | Forbidden |
304
+ | `NotFound` | 404 | Not Found |
305
+ | `MethodNotAllowed` | 405 | Method Not Allowed |
306
+ | `RequestTimeout` | 408 | Request Timeout |
307
+ | `Conflict` | 409 | Conflict |
308
+ | `Gone` | 410 | Gone |
309
+ | `LengthRequired` | 411 | Length Required |
310
+ | `ImATeapot` | 418 | I'm a Teapot |
311
+ | `UnprocessableContent` | 422 | Unprocessable Content |
312
+ | `TooManyRequests` | 429 | Too Many Requests |
313
+
314
+ ### Server Errors (5xx)
315
+
316
+ | Class | Status | Description |
317
+ |-------|--------|-------------|
318
+ | `InternalServerError` | 500 | Internal Server Error |
319
+ | `NotImplemented` | 501 | Not Implemented |
320
+ | `BadGateway` | 502 | Bad Gateway |
321
+ | `ServiceUnavailable` | 503 | Service Unavailable |
322
+
323
+ ### HTTP_ERROR Namespace
324
+
325
+ All error classes are available via the `HTTP_ERROR` namespace:
326
+
327
+ ```ts
328
+ import { HTTP_ERROR } from "@marianmeres/http-utils";
329
+
330
+ try {
331
+ await api.get("/resource");
332
+ } catch (error) {
333
+ if (error instanceof HTTP_ERROR.NotFound) {
334
+ console.log("Resource not found");
335
+ }
336
+ if (error instanceof HTTP_ERROR.HttpError) {
337
+ console.log("HTTP error:", error.status);
338
+ }
339
+ }
340
+ ```
341
+
342
+ ---
343
+
344
+ ## HTTP Status Codes
345
+
346
+ ### HTTP_STATUS Class
347
+
348
+ Access status codes by category or via direct shortcuts.
349
+
350
+ #### Categories
351
+
352
+ ```ts
353
+ HTTP_STATUS.INFO // 1xx Informational
354
+ HTTP_STATUS.SUCCESS // 2xx Success
355
+ HTTP_STATUS.REDIRECT // 3xx Redirection
356
+ HTTP_STATUS.ERROR_CLIENT // 4xx Client Error
357
+ HTTP_STATUS.ERROR_SERVER // 5xx Server Error
358
+ ```
359
+
360
+ #### Category Access
361
+
362
+ ```ts
363
+ HTTP_STATUS.SUCCESS.OK.CODE // 200
364
+ HTTP_STATUS.SUCCESS.OK.TEXT // "OK"
365
+ HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.CODE // 404
366
+ HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.TEXT // "Not Found"
367
+ ```
368
+
369
+ #### Direct Shortcuts
370
+
371
+ ```ts
372
+ HTTP_STATUS.OK // 200
373
+ HTTP_STATUS.CREATED // 201
374
+ HTTP_STATUS.ACCEPTED // 202
375
+ HTTP_STATUS.NO_CONTENT // 204
376
+ HTTP_STATUS.MOVED_PERMANENTLY // 301
377
+ HTTP_STATUS.FOUND // 302
378
+ HTTP_STATUS.NOT_MODIFIED // 304
379
+ HTTP_STATUS.BAD_REQUEST // 400
380
+ HTTP_STATUS.UNAUTHORIZED // 401
381
+ HTTP_STATUS.FORBIDDEN // 403
382
+ HTTP_STATUS.NOT_FOUND // 404
383
+ HTTP_STATUS.METHOD_NOT_ALLOWED // 405
384
+ HTTP_STATUS.CONFLICT // 409
385
+ HTTP_STATUS.GONE // 410
386
+ HTTP_STATUS.UNPROCESSABLE_CONTENT // 422
387
+ HTTP_STATUS.TOO_MANY_REQUESTS // 429
388
+ HTTP_STATUS.INTERNAL_SERVER_ERROR // 500
389
+ HTTP_STATUS.NOT_IMPLEMENTED // 501
390
+ HTTP_STATUS.SERVICE_UNAVAILABLE // 503
391
+ ```
392
+
393
+ #### findByCode(code)
394
+
395
+ Lookup status code by numeric value.
396
+
397
+ ```ts
398
+ static findByCode(code: number | string): {
399
+ CODE: number;
400
+ TEXT: string;
401
+ _TYPE: string;
402
+ _KEY: string;
403
+ } | null
404
+ ```
405
+
406
+ **Example:**
407
+ ```ts
408
+ const info = HTTP_STATUS.findByCode(404);
409
+ // { CODE: 404, TEXT: "Not Found", _TYPE: "ERROR_CLIENT", _KEY: "NOT_FOUND" }
410
+ ```
411
+
412
+ ---
413
+
414
+ ## Utilities
415
+
416
+ ### createHttpError
417
+
418
+ Creates an HTTP error from a status code and optional details.
419
+
420
+ ```ts
421
+ function createHttpError(
422
+ code: number | string,
423
+ message?: string | null,
424
+ body?: unknown,
425
+ cause?: unknown
426
+ ): HttpError
427
+ ```
428
+
429
+ Returns a specific error class for well-known status codes.
430
+
431
+ **Example:**
432
+ ```ts
433
+ const error = createHttpError(404, "User not found", { userId: 123 });
434
+ console.log(error instanceof NotFound); // true
435
+ console.log(error.status); // 404
436
+ console.log(error.body); // { userId: 123 }
437
+ ```
438
+
439
+ ### getErrorMessage
440
+
441
+ Extracts a human-readable error message from various error formats.
442
+
443
+ ```ts
444
+ function getErrorMessage(e: unknown, stripErrorPrefix?: boolean): string
445
+ ```
446
+
447
+ **Priority order:**
448
+ 1. `e.cause.message` / `e.cause.code` / `e.cause` (if string)
449
+ 2. `e.body.error.message` / `e.body.message` / `e.body.error` / `e.body` (if string)
450
+ 3. `e.message`
451
+ 4. `e.name`
452
+ 5. `e.toString()`
453
+ 6. `"Unknown Error"`
454
+
455
+ **Example:**
456
+ ```ts
457
+ import { getErrorMessage } from "@marianmeres/http-utils";
458
+
459
+ try {
460
+ await api.get("/fail");
461
+ } catch (error) {
462
+ console.log(getErrorMessage(error)); // "Not Found"
463
+ }
464
+ ```