@marianmeres/http-utils 1.22.0 → 2.0.1

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,59 +1,273 @@
1
1
  # @marianmeres/http-utils
2
2
 
3
- A few opinionated [sweet](https://en.wikipedia.org/wiki/Syntactic_sugar) `fetch` helpers.
3
+ Opinionated, lightweight HTTP client wrapper for `fetch` with type-safe errors and convenient defaults.
4
4
 
5
- ## Example
5
+ ## Features
6
6
 
7
- ```javascript
8
- import { HTTP_ERROR, HTTP_STATUS, createHttpApi } from '@marianmeres/http-utils';
7
+ - ðŸŽŊ **Type-safe HTTP errors** - Well-known status codes map to specific error classes
8
+ - 🔧 **Convenient defaults** - Auto JSON parsing, Bearer tokens, base URLs
9
+ - ðŸŠķ **Lightweight** - Zero dependencies, thin wrapper over native `fetch`
10
+ - ðŸŽĻ **Flexible error handling** - Three-tier error message extraction (local → factory → global)
11
+ - ðŸ“Ķ **Deno & Node.js** - Works in both runtimes
9
12
 
10
- // create api helper
11
- const api = createHttpApi(
12
- // optional base url
13
- 'https://api.example.com',
14
- // optional lazy evaluated default fetch params (can be overridden per call)
15
- async () => ({
16
- token: await getApiTokenFromDb() // example
17
- })
13
+ ## Installation
18
14
 
19
- // EXAMPLE: assuming `/resource` returns json {"some":"data"}
20
- const r = await api.get('/resource');
21
- assert(r.some === 'data');
15
+ ```shell
16
+ deno add jsr:@marianmeres/http-utils
17
+ ```
18
+
19
+ ```shell
20
+ npm install @marianmeres/http-utils
21
+ ```
22
+
23
+ ```ts
24
+ import { createHttpApi, HTTP_ERROR } from "@marianmeres/http-utils";
25
+ ```
26
+
27
+ ## Quick Start
28
+
29
+ ```ts
30
+ import { createHttpApi, HTTP_ERROR, NotFound } from "@marianmeres/http-utils";
31
+
32
+ // Create an API client with base URL
33
+ const api = createHttpApi("https://api.example.com", {
34
+ headers: { "Authorization": "Bearer your-token" }
35
+ });
36
+
37
+ // GET request (new options API - recommended)
38
+ const users = await api.get("/users", {
39
+ params: { headers: { "X-Custom": "value" } }
40
+ });
41
+
42
+ // POST request (new options API - recommended)
43
+ const newUser = await api.post("/users", {
44
+ data: { name: "John Doe" },
45
+ params: { headers: { "X-Custom": "value" } }
46
+ });
47
+
48
+ // Legacy API still works
49
+ const legacyUsers = await api.get("/users", { headers: { "X-Custom": "value" } });
50
+ const legacyUser = await api.post("/users", { name: "John Doe" });
51
+
52
+ // Error handling
53
+ try {
54
+ await api.get("/not-found");
55
+ } catch (error) {
56
+ if (error instanceof NotFound) {
57
+ console.log("Resource not found");
58
+ }
59
+ // or use the namespace
60
+ if (error instanceof HTTP_ERROR.NotFound) {
61
+ console.log(error.status); // 404
62
+ console.log(error.body); // Response body
63
+ }
64
+ }
65
+ ```
66
+
67
+ ## API Reference
68
+
69
+ ### `createHttpApi(base?, defaults?, errorExtractor?)`
70
+
71
+ Creates an HTTP API client.
72
+
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
+ ```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
99
+ });
100
+ ```
101
+
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
209
+
210
+ ```ts
211
+ const api = createHttpApi("https://api.example.com", async () => {
212
+ const token = await getToken(); // Fetch fresh token
213
+ return { headers: { "Authorization": `Bearer ${token}` } };
214
+ });
215
+ ```
216
+
217
+ ### Raw Response Access
218
+
219
+ ```ts
220
+ const response = await api.get("/users", { raw: true });
221
+ console.log(response instanceof Response); // true
222
+ const data = await response.json();
223
+ ```
224
+
225
+ ### Non-Throwing Errors
226
+
227
+ ```ts
228
+ const data = await api.get("/might-fail", { assert: false });
229
+ if (data.error) {
230
+ console.log("Request failed:", data.error.message);
231
+ }
232
+ ```
233
+
234
+ ### AbortController Support
235
+
236
+ ```ts
237
+ const controller = new AbortController();
238
+
239
+ setTimeout(() => controller.abort(), 5000);
240
+
241
+ await api.get("/slow-endpoint", { signal: controller.signal });
242
+ ```
243
+
244
+ ## Utilities
245
+
246
+ ### `getErrorMessage(error, stripErrorPrefix?)`
247
+
248
+ Extracts human-readable messages from any error format:
249
+
250
+ ```ts
251
+ import { getErrorMessage } from "@marianmeres/http-utils";
22
252
 
23
- // EXAMPLE: assuming `/foo` returns 404 header and json {"message":"hey"}
24
- // by default always throws
25
253
  try {
26
- const r = await api.get('/foo');
27
- } catch (e) {
28
- // see HTTP_ERROR for more
29
- assert(e instanceof HTTP_ERROR.NotFound);
30
- assert(e.toString() === 'HttpNotFoundError: Not Found');
31
- assert(e.status === HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.CODE);
32
- assert(e.statusText === HTTP_STATUS.ERROR_CLIENT.NOT_FOUND.TEXT);
33
- // `body` is a custom prop containing the raw http response body text (JSON.parse-d if available)
34
- assert(e.body.message === 'hey');
35
- // `cause` is a standart Error prop, containing here some default debug info
36
- assert(err.cause.response.headers)
254
+ await api.get("/fail");
255
+ } catch (error) {
256
+ console.log(getErrorMessage(error)); // "Not Found"
37
257
  }
258
+ ```
38
259
 
39
- // EXAMPLE: assuming `/foo` returns 404 header and json {"message":"hey"}
40
- // will not throw if we pass false flag
41
- const r = await api.get('/foo', { assert: false });
42
- assert(r.message === 'hey');
260
+ ### `createHttpError(code, message?, body?, cause?)`
43
261
 
44
- // EXAMPLE: assuming POST to `/resource` returns OK and json {"message":"created"}
45
- // the provided token below will override the one from the `getApiTokenFromDb()` call above
46
- const r = await api.post('/resource', { some: 'data' }, { token: 'my-api-token' });
47
- assert(r.message === 'created');
262
+ Manually create HTTP errors:
48
263
 
49
- // EXAMPLE: raw Response
50
- const r = await api.get('/resource', { raw: true });
51
- assert(r instanceof Response);
264
+ ```ts
265
+ import { createHttpError } from "@marianmeres/http-utils";
52
266
 
53
- // EXAMPLE: access to response headers
54
- let respHeaders = {};
55
- const r = await api.get('/resource', null, respHeaders);
56
- assert(Object.keys(respHeaders).length)
267
+ const error = createHttpError(404, "User not found", { userId: 123 });
268
+ throw error;
57
269
  ```
58
270
 
59
- See [`HTTP_STATUS`](./src/status.ts) and [`HTTP_ERROR`](./src/error.ts) for more.
271
+ ## License
272
+
273
+ MIT
package/dist/api.d.ts CHANGED
@@ -5,24 +5,148 @@ interface BaseParams {
5
5
  interface FetchParams {
6
6
  data?: any;
7
7
  token?: string | null;
8
- headers?: null | Record<string, string>;
9
- signal?: any;
10
- credentials?: null | 'omit' | 'same-origin' | 'include';
11
- raw?: null | boolean;
12
- assert?: null | boolean;
8
+ headers?: Record<string, string> | null;
9
+ signal?: AbortSignal;
10
+ credentials?: 'omit' | 'same-origin' | 'include' | null;
11
+ raw?: boolean | null;
12
+ assert?: boolean | null;
13
13
  }
14
14
  type BaseFetchParams = BaseParams & FetchParams;
15
- type ErrorMessageExtractor = (body: any, response: Response) => any;
16
- export declare function createHttpApi(base?: string | null, defaults?: Partial<BaseFetchParams> | (() => Promise<Partial<BaseFetchParams>>), factoryErrorMessageExtractor?: ErrorMessageExtractor | null | undefined): {
17
- get(path: string, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
18
- post(path: string, data?: any, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
19
- put(path: string, data?: any, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
20
- patch(path: string, data?: any, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
21
- del(path: string, data?: any, params?: FetchParams, respHeaders?: any, errorMessageExtractor?: ErrorMessageExtractor | null | undefined, _dumpParams?: boolean): Promise<any>;
22
- url: (path: string) => string;
23
- base: string | null | undefined;
24
- };
15
+ type ErrorMessageExtractor = (body: any, response: Response) => string;
16
+ type ResponseHeaders = Record<string, string | number>;
17
+ /**
18
+ * Options for HTTP GET requests (new cleaner API).
19
+ */
20
+ export interface GetOptions {
21
+ params?: FetchParams;
22
+ respHeaders?: ResponseHeaders | null;
23
+ errorExtractor?: ErrorMessageExtractor | null;
24
+ }
25
+ /**
26
+ * Options for HTTP POST/PUT/PATCH/DELETE requests (new cleaner API).
27
+ */
28
+ export interface DataOptions {
29
+ data?: any;
30
+ params?: FetchParams;
31
+ respHeaders?: ResponseHeaders | null;
32
+ errorExtractor?: ErrorMessageExtractor | null;
33
+ }
34
+ /**
35
+ * HTTP API client with convenient defaults and error handling.
36
+ */
37
+ export declare class HttpApi {
38
+ #private;
39
+ constructor(base?: string | null, defaults?: Partial<BaseFetchParams> | (() => Promise<Partial<BaseFetchParams>>), factoryErrorMessageExtractor?: ErrorMessageExtractor | null | undefined);
40
+ /**
41
+ * Performs a GET request (new options API - recommended).
42
+ *
43
+ * @param path - The request path (will be appended to base URL if set).
44
+ * @param options - Request options object.
45
+ * @returns The response body (auto-parsed as JSON if possible), or Response if `raw: true`.
46
+ * @throws {HttpError} When the response is not OK and `assert` is true (default).
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * const data = await api.get('/users', {
51
+ * params: { headers: { 'X-Custom': 'value' } },
52
+ * respHeaders: {}
53
+ * });
54
+ * ```
55
+ */
56
+ get(path: string, options: GetOptions): Promise<any>;
57
+ /**
58
+ * Performs a GET request (legacy API).
59
+ *
60
+ * @param path - The request path (will be appended to base URL if set).
61
+ * @param params - Optional fetch parameters.
62
+ * @param respHeaders - Optional object to be mutated with response headers.
63
+ * @param errorMessageExtractor - Optional custom error message extractor.
64
+ * @param _dumpParams - Internal parameter for testing.
65
+ * @returns The response body (auto-parsed as JSON if possible), or Response if `raw: true`.
66
+ * @throws {HttpError} When the response is not OK and `assert` is true (default).
67
+ */
68
+ get(path: string, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<any>;
69
+ /**
70
+ * Performs a POST request (new options API - recommended).
71
+ *
72
+ * @param path - The request path (will be appended to base URL if set).
73
+ * @param options - Request options object including data and params.
74
+ * @returns The response body (auto-parsed as JSON if possible), or Response if `raw: true`.
75
+ * @throws {HttpError} When the response is not OK and `assert` is true (default).
76
+ *
77
+ * @example
78
+ * ```ts
79
+ * await api.post('/users', {
80
+ * data: { name: 'John' },
81
+ * params: { headers: { 'X-Custom': 'value' } },
82
+ * respHeaders: {}
83
+ * });
84
+ * ```
85
+ */
86
+ post(path: string, options: DataOptions): Promise<any>;
87
+ /**
88
+ * Performs a POST request (legacy API).
89
+ *
90
+ * @param path - The request path (will be appended to base URL if set).
91
+ * @param data - Request body data.
92
+ * @param params - Optional fetch parameters.
93
+ * @param respHeaders - Optional object to be mutated with response headers.
94
+ * @param errorMessageExtractor - Optional custom error message extractor.
95
+ * @param _dumpParams - Internal parameter for testing.
96
+ * @returns The response body (auto-parsed as JSON if possible), or Response if `raw: true`.
97
+ * @throws {HttpError} When the response is not OK and `assert` is true (default).
98
+ */
99
+ post(path: string, data?: any, params?: FetchParams, respHeaders?: ResponseHeaders | null, errorMessageExtractor?: ErrorMessageExtractor | null, _dumpParams?: boolean): Promise<any>;
100
+ /** Performs a PUT request (new options API). @see post */
101
+ put(path: string, options: DataOptions): Promise<any>;
102
+ /** 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>;
104
+ /** Performs a PATCH request (new options API). @see post */
105
+ patch(path: string, options: DataOptions): Promise<any>;
106
+ /** 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>;
108
+ /**
109
+ * Performs a DELETE request (new options API).
110
+ * Note: Request body in DELETE is allowed per HTTP spec.
111
+ * @see post
112
+ */
113
+ del(path: string, options: DataOptions): Promise<any>;
114
+ /** 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>;
116
+ /**
117
+ * Helper method to build the full URL from a path.
118
+ *
119
+ * @param path - The path to resolve (absolute URLs are returned as-is).
120
+ * @returns The resolved URL (base + path, or just path if it's already absolute).
121
+ */
122
+ url(path: string): string;
123
+ /**
124
+ * Get or set the base URL for all requests.
125
+ */
126
+ get base(): string | null | undefined;
127
+ set base(v: string | null | undefined);
128
+ }
129
+ /**
130
+ * Creates an HTTP API client with convenient defaults and error handling.
131
+ *
132
+ * @param base - Optional base URL to prepend to all requests. Can be changed later via the `base` property.
133
+ * @param defaults - Optional default parameters to merge with each request. Can be an object or async function returning an object.
134
+ * @param factoryErrorMessageExtractor - Optional function to extract error messages from failed responses.
135
+ *
136
+ * @returns An HttpApi instance with methods: get, post, put, patch, del, url, base.
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * const api = createHttpApi('https://api.example.com', {
141
+ * headers: { 'Authorization': 'Bearer token' }
142
+ * });
143
+ *
144
+ * const data = await api.get('/users');
145
+ * await api.post('/users', { name: 'John' });
146
+ * ```
147
+ */
148
+ export declare function createHttpApi(base?: string | null, defaults?: Partial<BaseFetchParams> | (() => Promise<Partial<BaseFetchParams>>), factoryErrorMessageExtractor?: ErrorMessageExtractor | null | undefined): HttpApi;
25
149
  export declare namespace createHttpApi {
26
- var defaultErrorMessageExtractor: ErrorMessageExtractor | null | undefined;
150
+ var defaultErrorMessageExtractor: ErrorMessageExtractor;
27
151
  }
28
152
  export {};