@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 +332 -72
- package/dist/cjs/client/index.cjs +236 -199
- package/dist/cjs/client/index.cjs.map +4 -4
- package/dist/cjs/index.cjs +263 -208
- package/dist/cjs/index.cjs.map +4 -4
- package/dist/cjs/server/index.cjs +102 -56
- package/dist/cjs/server/index.cjs.map +4 -4
- package/dist/esm/{chunk-72RXZFQZ.js → chunk-OQFXJL6I.js} +24 -6
- package/dist/esm/chunk-OQFXJL6I.js.map +7 -0
- package/dist/esm/chunk-VHRZDEWF.js +309 -0
- package/dist/esm/chunk-VHRZDEWF.js.map +7 -0
- package/dist/esm/{chunk-7K2N7A5V.js → chunk-ZSJH7BQI.js} +97 -87
- package/dist/esm/chunk-ZSJH7BQI.js.map +7 -0
- package/dist/esm/client/index.mjs +4 -4
- package/dist/esm/index.mjs +7 -5
- package/dist/esm/server/index.mjs +4 -4
- package/package.json +1 -1
- package/dist/esm/chunk-72RXZFQZ.js.map +0 -7
- package/dist/esm/chunk-7K2N7A5V.js.map +0 -7
- package/dist/esm/chunk-CYUBH2V3.js +0 -280
- package/dist/esm/chunk-CYUBH2V3.js.map +0 -7
package/README.md
CHANGED
|
@@ -1,91 +1,351 @@
|
|
|
1
|
-
# @randajan/api-kit
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/@randajan/api-kit) [](https://standardjs.com)
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Installation
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
```
|
|
16
|
-
import createApi from "@randajan/api-kit/server";
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```
|
|
35
|
-
import
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
1
|
+
# @randajan/api-kit
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@randajan/api-kit) [](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
|
-
|
|
77
|
+
Returns a frozen response object unless `resultOnly:true` or `throwError:true` changes the flow.
|
|
78
|
+
|
|
79
|
+
## Response Contract
|
|
58
80
|
|
|
59
|
-
|
|
81
|
+
Client calls return this normalized shape:
|
|
60
82
|
|
|
61
|
-
```
|
|
83
|
+
```js
|
|
62
84
|
{
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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)
|