@randajan/api-kit 2.0.0 → 3.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,91 +1,351 @@
1
- # @randajan/api-kit
2
-
3
- [![NPM](https://img.shields.io/npm/v/@randajan/api-kit.svg)](https://www.npmjs.com/package/@randajan/api-kit) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
4
-
5
- JavaScript library for rendering structured data in HTML format. It supports automatic detection of tabular structures and visualization of deeply nested objects. It is ideal for debugging, admin interfaces, or displaying API responses.
6
-
7
- ## Installation
8
-
9
-
10
- ## Usage
11
-
12
- ### Server
13
- You can use the library as an ES module:
14
-
15
- ```javascript
16
- import createApi from "@randajan/api-kit/server";
17
-
18
- const apiReponder = ({
19
- code:0,
20
- isAsync:false,
21
- timestamp:false,
22
- trait:(opt)=>opt,
23
- onOk:(resp, opt)=>{},
24
- onError:(resp, opt)=>{ },
25
- throwError:false
26
- });
27
-
28
-
29
- ```
30
-
31
- ### Client
32
- You can use the library as an ES module:
33
-
34
- ```javascript
35
- import createFetch from "@randajan/api-kit/client";
36
-
37
- const apiFetch = createFetch({
38
- code:0,
39
- url:"",
40
- fetch:globalThis.fetch,
41
- query:{},
42
- parseHeaders:false, //if true it will parse response headers
43
- resultOnly:false, //if true it will return only result
44
- timestamp:false, //if true it will calculate timestamps
45
- parseBody:(body)=>body, //custom parser for non api-kit server fetches only
46
- trait:(opt)=>opt,
47
- onOk:(resp, opt)=>{},
48
- onError:(resp, opt)=>{ },
49
- requestType:"json", // json or form, the 'type' property is fallback
50
- responseType:"json", // json or form, the 'type' property is fallback
51
- throwError:false
1
+ # @randajan/api-kit
2
+
3
+ [![NPM](https://img.shields.io/npm/v/@randajan/api-kit.svg)](https://www.npmjs.com/package/@randajan/api-kit) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
4
+
5
+ `@randajan/api-kit` wraps server handlers and client `fetch` calls into one predictable response and error contract. It keeps successful results, known HTTP errors, foreign API responses, decode failures, timeouts, and aborts distinguishable without forcing every call site to use `try/catch`.
6
+
7
+ ## Installation
8
+
9
+ ```sh
10
+ npm install @randajan/api-kit
11
+ ```
12
+
13
+ ## Imports
14
+
15
+ ```js
16
+ import createApi, { HttpError } from "@randajan/api-kit/server";
17
+ import createFetch, { FetchError } from "@randajan/api-kit/client";
18
+ ```
19
+
20
+ For shared packages or full-stack code:
21
+
22
+ ```js
23
+ import { createApi, createFetch, HttpError, FetchError } from "@randajan/api-kit";
24
+ ```
25
+
26
+ `HttpError` is exported by the server subpath and the root package. The client subpath exports `FetchError`.
27
+
28
+ ## Quick Start
29
+
30
+ ### Server
31
+
32
+ Do: wrap handler logic with `api(...)`, then send the returned body with `body.statusCode`.
33
+
34
+ ```js
35
+ import express from "express";
36
+ import createApi, { HttpError } from "@randajan/api-kit/server";
37
+
38
+ const app = express();
39
+ const api = createApi({ timetrack:true });
40
+
41
+ app.get("/users/:id", (req, res) => {
42
+ const body = api.code(10, () => {
43
+ if (!req.params.id) {
44
+ throw HttpError.code(1, "Missing user id", 400);
45
+ }
46
+
47
+ return { id:req.params.id, name:"Ada" };
48
+ });
49
+
50
+ res.status(body.statusCode).json(body);
51
+ });
52
+ ```
53
+
54
+ Returns an api-kit response body containing a package marker, `statusCode`, and either `result` or `error`.
55
+
56
+ ### Client
57
+
58
+ Do: create a configured fetch wrapper and read `isOk`, `result`, and `error`.
59
+
60
+ ```js
61
+ import createFetch from "@randajan/api-kit/client";
62
+
63
+ const api = createFetch({
64
+ url:"https://api.example.com/",
65
+ timeout:10000
52
66
  });
53
67
 
68
+ const body = await api.get("users/42");
54
69
 
70
+ if (body.isOk) {
71
+ console.log(body.result);
72
+ } else {
73
+ console.error(body.statusCode, body.error);
74
+ }
55
75
  ```
56
76
 
57
- ### Response contract
77
+ Returns a frozen response object unless `resultOnly:true` or `throwError:true` changes the flow.
78
+
79
+ ## Response Contract
58
80
 
59
- Unless `resultOnly` or `throwError` changes the flow, client calls return a response object with these flags:
81
+ Client calls return this normalized shape:
60
82
 
61
- ```javascript
83
+ ```js
62
84
  {
63
- isOk:true, //true when response has no error
64
- isRemote:true, //true when fetch received an HTTP response object
65
- isApiKit:true, //true when response contains the api-kit marker
85
+ "@randajan/api-kit":"3.0.0", // present only for api-kit server responses
86
+ isOk:true, // true when there is no error
87
+ isRemote:true, // true when fetch received an HTTP Response
88
+ isApiKit:true, // true when the response had the api-kit marker
89
+ apiKitDiff:undefined, // "major", "minor", or "patch" when versions differ
66
90
  statusCode:200,
67
91
  result:{},
68
- error:null
92
+ error:undefined,
93
+ headers:undefined, // present when parseHeaders:true
94
+ time:undefined // present when timetrack:true
69
95
  }
70
96
  ```
71
97
 
72
- Common combinations:
98
+ Common flag combinations:
99
+
100
+ | `isOk` | `isRemote` | `isApiKit` | Meaning |
101
+ | --- | --- | --- | --- |
102
+ | `true` | `true` | `true` | Successful api-kit response |
103
+ | `false` | `true` | `true` | Api-kit error response |
104
+ | `true` | `true` | `false` | Successful foreign API response |
105
+ | `false` | `true` | `false` | Foreign HTTP/API error or remote decode/read error |
106
+ | `false` | `false` | `false` | Local transport error, timeout, abort, or fetch failure |
107
+
108
+ Notes:
109
+
110
+ - `statusCode` is taken from `error.httpStatusCode` when the error has one, otherwise from the HTTP response.
111
+ - Empty successful responses such as `204`, `HEAD 200`, and `304` return `isOk:true` with `result:undefined`.
112
+ - Non-api-kit successful responses are returned as `result` after response decoding.
113
+
114
+ ## Errors
115
+
116
+ ### Known Server Errors
117
+
118
+ Use `HttpError.code(code, message, httpStatusCode, options)` for errors that should cross the API boundary with a known HTTP status.
119
+
120
+ ```js
121
+ throw HttpError.code(20, "Email already exists", 409, {
122
+ detail:{ field:"email" },
123
+ cause:error
124
+ });
125
+ ```
126
+
127
+ `detail` is public structured data. `cause` is hidden by default unless the server option `exposeCause:true` is enabled.
128
+
129
+ ### Local Client Errors
130
+
131
+ Client transport failures are represented as `FetchError` instances:
132
+
133
+ ```js
134
+ import createFetch, { FetchError } from "@randajan/api-kit/client";
135
+
136
+ const api = createFetch({
137
+ normalizeError(error) {
138
+ if (error.name === "QuotaExceededError") {
139
+ return FetchError.code(90, "Client quota exceeded", { cause:error });
140
+ }
141
+ }
142
+ });
143
+ ```
144
+
145
+ Return a `FetchError` from client `normalizeError`. Returning anything else falls back to an `Unknown` `FetchError`.
146
+
147
+ ### Throwing Instead Of Returning
148
+
149
+ By default, api-kit returns response objects. Use `throwError:true` when you want errors to be thrown after normalization.
150
+
151
+ ```js
152
+ const api = createFetch({ url:"https://api.example.com/", throwError:true });
153
+
154
+ try {
155
+ const body = await api.get("users/42");
156
+ console.log(body.result);
157
+ } catch (error) {
158
+ console.error(error.code, error.message);
159
+ }
160
+ ```
161
+
162
+ The same option exists on the server wrapper.
163
+
164
+ ### Normalizing Known Errors
165
+
166
+ Use `normalizeError(error, opt)` to convert framework or application errors into api-kit errors before the default fallback runs.
167
+
168
+ ```js
169
+ const api = createApi({
170
+ normalizeError(error) {
171
+ if (error.name === "ValidationError") {
172
+ return HttpError.code(30, "Validation failed", 422, {
173
+ detail:error.fields,
174
+ cause:error
175
+ });
176
+ }
177
+ }
178
+ });
179
+ ```
180
+
181
+ Server `normalizeError` should return `HttpError`. Client `normalizeError` should return `FetchError`. If the hook throws or returns another value, api-kit falls back to `Unknown`.
182
+
183
+ ### Exposing Cause
184
+
185
+ Use `exposeCause:true` only for trusted environments. It can expose server-side error messages and stacks in the serialized API response.
186
+
187
+ ```js
188
+ const api = createApi({ exposeCause:process.env.NODE_ENV !== "production" });
189
+ ```
190
+
191
+ ## Options
192
+
193
+ Options can be passed to the constructor and overridden per call. Client options are shallow-merged, with `body`, `params`, and `headers` merged separately.
194
+
195
+ ### Shared Options
196
+
197
+ | Option | Type | Description |
198
+ | --- | --- | --- |
199
+ | `timetrack` | `boolean` | Adds timing metadata. Server records handler time; client can calculate network timing from server time. |
200
+ | `trait` | `(opt) => opt` | Last chance to inspect or mutate options before execution. |
201
+ | `onOk` | `(body, opt) => void` | Called when normalized response has no `error`. |
202
+ | `onError` | `(body, opt) => void` | Called when normalized response has `error`. |
203
+ | `throwError` | `boolean` | Throws `body.error` instead of returning an error response. |
204
+ | `normalizeError` | `(error, opt) => Error` | Converts known errors before api-kit creates its default unknown error. |
205
+
206
+ ### Server Options
207
+
208
+ | Option | Type | Description |
209
+ | --- | --- | --- |
210
+ | `code` | `string` or `number` | Prefix for server error codes. Also set by `api.code(code, exe, opt)`. |
211
+ | `isAsync` | `boolean` | Forces async handler execution. Promise results are also detected without this option. |
212
+ | `exposeCause` | `boolean` | Includes serialized `cause` in `HttpError.toJSON()`. |
213
+
214
+ ### Client Options
215
+
216
+ | Option | Type | Description |
217
+ | --- | --- | --- |
218
+ | `fetch` | `Function` | Custom fetch implementation. Constructor-only; removed from stored config. |
219
+ | `url` | `string` | Base or prefix URL joined with the call URL. |
220
+ | `params` | `object` | Query params appended to the final URL. `null` and `undefined` values are skipped. |
221
+ | `method` | `string` | HTTP method. Set automatically by `.get()`, `.post()`, `.put()`, `.delete()`, `.patch()`, `.head()`, and `.options()`. |
222
+ | `body` | `object` or any | Request body before encoding. |
223
+ | `headers` | `object` | Request headers. api-kit also sets `Content-Type` and `Accept` from the selected type. |
224
+ | `requestType` | `string` | Encoder for request body. Native values: `json`, `form`. |
225
+ | `responseType` | `string` | Decoder for response body. Native values: `json`, `form`. |
226
+ | `type` | `string` | Fallback for both `requestType` and `responseType`. |
227
+ | `types` | `object` | Custom type definitions: `{ mime, encode, decode }`. |
228
+ | `timeout` | `number` | Timeout in milliseconds. Creates an abort signal when greater than `0`. |
229
+ | `isAbortable` | `boolean` | Adds `.abort()` to the returned promise. |
230
+ | `parseHeaders` | `boolean` | Adds frozen parsed response headers to `body.headers`. |
231
+ | `resultOnly` | `boolean` | Returns only `body.result` after normalization. |
232
+
233
+ ### Runtime Fields
234
+
235
+ These fields are created or changed by api-kit during execution. Do not set them manually unless you are intentionally extending internals.
236
+
237
+ | Field | Created by | Description |
238
+ | --- | --- | --- |
239
+ | `startAt` | `start(opt)` | Timestamp used by `timetrack`. |
240
+ | `decodeBody` | `attachBodyDecoder()` | Response body decoder selected from `responseType` or `type`. |
241
+ | `abortController` | client prepare step | Controller used by timeout and abortable requests. |
242
+ | `signal` | client prepare step | Fetch abort signal. |
243
+ | `timeoutId` | client prepare step | Timer cleared after the request settles. |
244
+
245
+ The client also mutates prepared `url`, `method`, `headers`, and encoded `body`.
246
+
247
+ ## Recipes
248
+
249
+ ### POST JSON
250
+
251
+ Do:
252
+
253
+ ```js
254
+ const body = await api.post("users", {
255
+ body:{ name:"Ada" }
256
+ });
257
+ ```
258
+
259
+ Returns an api-kit response object. JSON is the default request and response type.
260
+
261
+ ### POST Form Data
262
+
263
+ Do:
264
+
265
+ ```js
266
+ const body = await api.post("login", {
267
+ requestType:"form",
268
+ body:{ username:"ada", password:"secret" }
269
+ });
270
+ ```
271
+
272
+ Notes: `requestType:"form"` uses `application/x-www-form-urlencoded`.
273
+
274
+ ### Timeout And Abort
275
+
276
+ Do:
277
+
278
+ ```js
279
+ const request = api.get("slow", {
280
+ timeout:1000,
281
+ isAbortable:true
282
+ });
283
+
284
+ request.abort();
73
285
 
74
- ```text
75
- isOk isRemote isApiKit
76
- true true true successful api-kit response
77
- false true true api-kit error response
78
- false true false foreign API or HTTP error response
79
- false false false local transport error, timeout or abort
286
+ const body = await request;
80
287
  ```
81
288
 
289
+ Returns a local transport error response with `isRemote:false` when the request is aborted before an HTTP response is received.
82
290
 
291
+ ### Parse Response Headers
292
+
293
+ Do:
294
+
295
+ ```js
296
+ const body = await api.get("users/42", {
297
+ parseHeaders:true
298
+ });
299
+
300
+ console.log(body.headers);
301
+ ```
302
+
303
+ Returns `body.headers` as a frozen plain object.
304
+
305
+ ### Custom Content Type
306
+
307
+ Do:
308
+
309
+ ```js
310
+ const api = createFetch({
311
+ url:"https://api.example.com/",
312
+ responseType:"text",
313
+ types:{
314
+ text:{
315
+ mime:"text/plain",
316
+ encode:value=>String(value),
317
+ decode:text=>text
318
+ }
319
+ }
320
+ });
321
+
322
+ const body = await api.get("status");
323
+ ```
324
+
325
+ Notes: Custom request types require `mime` and `encode`. Custom response types require `mime` and `decode`.
326
+
327
+ ### Map A Known App Error
328
+
329
+ Do:
330
+
331
+ ```js
332
+ class NotFoundError extends Error {}
333
+
334
+ const api = createApi({
335
+ normalizeError(error) {
336
+ if (error instanceof NotFoundError) {
337
+ return HttpError.code(40, "Not found", 404, { cause:error });
338
+ }
339
+ }
340
+ });
341
+ ```
342
+
343
+ Returns a regular api-kit error response with HTTP status `404`.
83
344
 
84
345
  ## Support
85
-
86
- If you have any questions or suggestions for improvements, feel free to open an issue in the repository.
87
-
88
-
89
- ## License
90
-
91
- MIT © [randajan](https://github.com/randajan)
346
+
347
+ Open an issue in the repository for questions, bugs, or improvement ideas.
348
+
349
+ ## License
350
+
351
+ MIT (c) [randajan](https://github.com/randajan)