@api-wrappers/api-core 0.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 +463 -0
- package/dist/index.cjs +994 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +755 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +755 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +973 -0
- package/dist/index.mjs.map +1 -0
- package/docs/README.md +42 -0
- package/docs/getting-started.md +93 -0
- package/docs/guides/built-in-plugins.md +119 -0
- package/docs/guides/error-handling.md +72 -0
- package/docs/guides/graphql.md +81 -0
- package/docs/guides/plugins.md +122 -0
- package/docs/guides/rest-requests.md +113 -0
- package/docs/guides/testing.md +88 -0
- package/docs/reference/client.md +53 -0
- package/docs/reference/configuration.md +83 -0
- package/docs/reference/exports.md +74 -0
- package/package.json +54 -0
package/README.md
ADDED
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
# @api-wrappers/api-core
|
|
2
|
+
|
|
3
|
+
Shared TypeScript HTTP runtime for API wrapper libraries.
|
|
4
|
+
|
|
5
|
+
`@api-wrappers/api-core` gives wrapper packages a small, predictable foundation
|
|
6
|
+
for request execution, retries, timeouts, auth headers, caching, rate limiting,
|
|
7
|
+
GraphQL requests, custom transports, and plugin-based request/response
|
|
8
|
+
middleware.
|
|
9
|
+
|
|
10
|
+
It is designed for packages that expose domain-specific clients while keeping
|
|
11
|
+
their internal HTTP layer consistent and testable.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- Typed REST helpers: `get`, `post`, `put`, `patch`, `delete`, `head`,
|
|
16
|
+
`options`, and `request`.
|
|
17
|
+
- `requestWithResponse` for wrappers that need response headers, status, or
|
|
18
|
+
plugin metadata.
|
|
19
|
+
- GraphQL helper with typed `data` and `variables`.
|
|
20
|
+
- Deterministic plugin lifecycle with `setup`, `beforeRequest`,
|
|
21
|
+
`afterResponse`, `onError`, and `dispose`.
|
|
22
|
+
- Built-in auth, cache, logger, rate-limit, retry, and timeout plugins.
|
|
23
|
+
- Fetch transport with JSON bodies, raw string bodies, abort signals, and
|
|
24
|
+
timeout handling.
|
|
25
|
+
- Query string support for primitives and repeated array values.
|
|
26
|
+
- ESM and CommonJS builds with TypeScript declarations.
|
|
27
|
+
|
|
28
|
+
## Requirements
|
|
29
|
+
|
|
30
|
+
- TypeScript 5+
|
|
31
|
+
- A runtime with `fetch`, `Request`, `Response`, and `AbortController`
|
|
32
|
+
available. Modern Node, Bun, browsers, and edge runtimes satisfy this.
|
|
33
|
+
- For older runtimes, pass a custom `fetch` implementation or a full custom
|
|
34
|
+
`Transport`.
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
bun add @api-wrappers/api-core
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install @api-wrappers/api-core
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Quick Start
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
import {
|
|
50
|
+
createAuthPlugin,
|
|
51
|
+
createClient,
|
|
52
|
+
createRetryPlugin,
|
|
53
|
+
createTimeoutPlugin,
|
|
54
|
+
} from "@api-wrappers/api-core";
|
|
55
|
+
|
|
56
|
+
interface User {
|
|
57
|
+
id: string;
|
|
58
|
+
name: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const client = createClient({
|
|
62
|
+
baseUrl: "https://api.example.com/v1",
|
|
63
|
+
defaultHeaders: { accept: "application/json" },
|
|
64
|
+
plugins: [
|
|
65
|
+
createAuthPlugin(() => process.env.API_TOKEN),
|
|
66
|
+
createRetryPlugin({ maxAttempts: 3, delayMs: 300 }),
|
|
67
|
+
createTimeoutPlugin({ timeoutMs: 30_000 }),
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const user = await client.get<User>("/users/123");
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`baseUrl` and request paths are slash-safe:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
client.get("/users");
|
|
78
|
+
client.get("users");
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Both work with `baseUrl: "https://api.example.com/v1/"`.
|
|
82
|
+
|
|
83
|
+
## Client Configuration
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { createClient } from "@api-wrappers/api-core";
|
|
87
|
+
|
|
88
|
+
const client = createClient({
|
|
89
|
+
baseUrl: "https://api.example.com",
|
|
90
|
+
defaultHeaders: {
|
|
91
|
+
accept: "application/json",
|
|
92
|
+
},
|
|
93
|
+
timeoutMs: 10_000,
|
|
94
|
+
retry: {
|
|
95
|
+
maxAttempts: 2,
|
|
96
|
+
delayMs: 250,
|
|
97
|
+
jitter: true,
|
|
98
|
+
retriableStatusCodes: [429, 500, 502, 503, 504],
|
|
99
|
+
},
|
|
100
|
+
plugins: [],
|
|
101
|
+
logger: console,
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
| Option | Purpose |
|
|
106
|
+
| --- | --- |
|
|
107
|
+
| `baseUrl` | Base URL prepended to relative request paths. |
|
|
108
|
+
| `defaultHeaders` | Headers merged into every request. Per-request headers win. |
|
|
109
|
+
| `timeoutMs` | Default request timeout. Can be overridden per request. |
|
|
110
|
+
| `retry` | Global retry policy. Can be overridden by `createRetryPlugin`. |
|
|
111
|
+
| `plugins` | Plugin list for auth, cache, logging, rate limiting, etc. |
|
|
112
|
+
| `transport` | Full request executor override, useful in tests. |
|
|
113
|
+
| `fetch` | Custom fetch implementation used by the default transport. |
|
|
114
|
+
| `logger` | Internal diagnostics logger. Defaults to `console`. |
|
|
115
|
+
|
|
116
|
+
## Requests
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
await client.get<SearchResult>("/search", {
|
|
120
|
+
query: {
|
|
121
|
+
q: "alien",
|
|
122
|
+
page: 2,
|
|
123
|
+
with_genres: [878, 12],
|
|
124
|
+
skip: undefined,
|
|
125
|
+
},
|
|
126
|
+
headers: { accept: "application/json" },
|
|
127
|
+
timeoutMs: 5_000,
|
|
128
|
+
signal: abortController.signal,
|
|
129
|
+
tags: ["search"],
|
|
130
|
+
cacheKey: "search:alien:2",
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Query values can be strings, numbers, booleans, nullish values, or arrays of
|
|
135
|
+
those primitives. `null` and `undefined` are skipped. Arrays are encoded as
|
|
136
|
+
repeated query parameters:
|
|
137
|
+
|
|
138
|
+
```txt
|
|
139
|
+
?with_genres=878&with_genres=12
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Request Methods
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
client.get<T>(path, options);
|
|
146
|
+
client.post<T>(path, body, options);
|
|
147
|
+
client.put<T>(path, body, options);
|
|
148
|
+
client.patch<T>(path, body, options);
|
|
149
|
+
client.delete<T>(path, options);
|
|
150
|
+
client.head<T>(path, options);
|
|
151
|
+
client.options<T>(path, options);
|
|
152
|
+
client.request<T>(path, { method: "POST", body });
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Plain objects and arrays are JSON encoded. Strings and native `BodyInit`
|
|
156
|
+
values are sent as-is, which supports APIs that expect text query languages:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
const games = await client.post<Game[]>(
|
|
160
|
+
"/games",
|
|
161
|
+
"fields name,rating; limit 10;",
|
|
162
|
+
{
|
|
163
|
+
headers: {
|
|
164
|
+
"content-type": "text/plain",
|
|
165
|
+
accept: "application/json",
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Response Metadata
|
|
172
|
+
|
|
173
|
+
Use `requestWithResponse` when a wrapper needs more than the parsed body:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
const result = await client.requestWithResponse<MoviePage>("/movie/popular");
|
|
177
|
+
|
|
178
|
+
result.data;
|
|
179
|
+
result.response.status;
|
|
180
|
+
result.response.headers.get("x-ratelimit-remaining");
|
|
181
|
+
result.request.url;
|
|
182
|
+
result.meta["cache.served"];
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## GraphQL
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
interface GetMediaQuery {
|
|
189
|
+
Media: { id: number; title: { romaji: string } };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface GetMediaVariables {
|
|
193
|
+
id: number;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const data = await client.graphql<GetMediaQuery, GetMediaVariables>("/", {
|
|
197
|
+
query: `
|
|
198
|
+
query GetMedia($id: Int) {
|
|
199
|
+
Media(id: $id) { id title { romaji } }
|
|
200
|
+
}
|
|
201
|
+
`,
|
|
202
|
+
variables: { id: 1 },
|
|
203
|
+
operationName: "GetMedia",
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
console.log(data.Media.title.romaji);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
GraphQL uses the same transport, plugin lifecycle, retry policy, timeout
|
|
210
|
+
handling, and error classes as REST requests.
|
|
211
|
+
|
|
212
|
+
## Built-In Plugins
|
|
213
|
+
|
|
214
|
+
Plugins are ordinary objects that run through a deterministic lifecycle. Lower
|
|
215
|
+
priority values run earlier in `beforeRequest`; higher values run earlier in
|
|
216
|
+
`afterResponse`.
|
|
217
|
+
|
|
218
|
+
### Auth
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
createAuthPlugin("static-token");
|
|
222
|
+
createAuthPlugin(() => tokenStore.getAccessToken());
|
|
223
|
+
createAuthPlugin({
|
|
224
|
+
getToken: () => apiKey,
|
|
225
|
+
headerName: "x-api-key",
|
|
226
|
+
scheme: null,
|
|
227
|
+
});
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
The default header is:
|
|
231
|
+
|
|
232
|
+
```txt
|
|
233
|
+
authorization: Bearer <token>
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Retry
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
createRetryPlugin({
|
|
240
|
+
maxAttempts: 3,
|
|
241
|
+
delayMs: 300,
|
|
242
|
+
jitter: true,
|
|
243
|
+
retriableStatusCodes: [429, 500, 502, 503, 504],
|
|
244
|
+
});
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
`429` responses respect `retry-after` when present. Numeric values are treated
|
|
248
|
+
as seconds; HTTP-date values are also supported.
|
|
249
|
+
|
|
250
|
+
### Timeout
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
createTimeoutPlugin({ timeoutMs: 30_000 });
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Timeouts throw `TimeoutError`.
|
|
257
|
+
|
|
258
|
+
### Rate Limit
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
createRateLimitPlugin({
|
|
262
|
+
maxConcurrent: 4,
|
|
263
|
+
minTimeMs: 250,
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
createRateLimitPlugin({
|
|
269
|
+
maxRequestsPerInterval: 30,
|
|
270
|
+
intervalMs: 60_000,
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
The limiter releases slots on successful responses, transport failures, and
|
|
275
|
+
plugin failures.
|
|
276
|
+
|
|
277
|
+
### Cache
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { createCachePlugin, MemoryStore } from "@api-wrappers/api-core";
|
|
281
|
+
|
|
282
|
+
const cache = createCachePlugin({
|
|
283
|
+
store: new MemoryStore(),
|
|
284
|
+
ttlMs: 60_000,
|
|
285
|
+
methods: ["GET"],
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
const client = createClient({
|
|
289
|
+
baseUrl: "https://api.example.com",
|
|
290
|
+
plugins: [cache],
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
await client.get("/users/1", { tags: ["user"] });
|
|
294
|
+
await cache.invalidate("GET:https://api.example.com/users/1");
|
|
295
|
+
await cache.invalidateByTag("user");
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Cache hits skip the transport and set `meta["cache.served"]`.
|
|
299
|
+
|
|
300
|
+
### Logger
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
createLoggerPlugin({
|
|
304
|
+
logRequest: true,
|
|
305
|
+
logResponse: true,
|
|
306
|
+
logError: true,
|
|
307
|
+
logger: console,
|
|
308
|
+
});
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
Pass a structured logger or no-op logger to control diagnostics.
|
|
312
|
+
|
|
313
|
+
## Custom Plugins
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
import type { ApiPlugin } from "@api-wrappers/api-core";
|
|
317
|
+
|
|
318
|
+
export function createClientIdPlugin(clientId: string): ApiPlugin {
|
|
319
|
+
return {
|
|
320
|
+
name: "client-id",
|
|
321
|
+
priority: 2,
|
|
322
|
+
beforeRequest(ctx) {
|
|
323
|
+
return {
|
|
324
|
+
...ctx,
|
|
325
|
+
headers: {
|
|
326
|
+
...ctx.headers,
|
|
327
|
+
"client-id": clientId,
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
Hooks may return a new context or `undefined` to keep the current one.
|
|
336
|
+
|
|
337
|
+
| Hook | When it runs |
|
|
338
|
+
| --- | --- |
|
|
339
|
+
| `setup(client)` | Once, lazily before the first request. |
|
|
340
|
+
| `beforeRequest(ctx)` | Before transport execution. |
|
|
341
|
+
| `afterResponse(ctx)` | After response parsing. |
|
|
342
|
+
| `onError(error, ctx)` | For transport, HTTP, and plugin failures. |
|
|
343
|
+
| `dispose()` | When `client.dispose()` is called. |
|
|
344
|
+
|
|
345
|
+
Read [docs/guides/plugins.md](docs/guides/plugins.md) for the full plugin
|
|
346
|
+
contract.
|
|
347
|
+
|
|
348
|
+
## Error Handling
|
|
349
|
+
|
|
350
|
+
```ts
|
|
351
|
+
import {
|
|
352
|
+
ApiError,
|
|
353
|
+
GraphQLRequestError,
|
|
354
|
+
RateLimitError,
|
|
355
|
+
TimeoutError,
|
|
356
|
+
} from "@api-wrappers/api-core";
|
|
357
|
+
|
|
358
|
+
try {
|
|
359
|
+
await client.get("/resource");
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error instanceof RateLimitError) {
|
|
362
|
+
console.log(error.retryAfterMs);
|
|
363
|
+
} else if (error instanceof TimeoutError) {
|
|
364
|
+
console.log("timed out");
|
|
365
|
+
} else if (error instanceof GraphQLRequestError) {
|
|
366
|
+
console.log(error.graphqlErrors);
|
|
367
|
+
} else if (error instanceof ApiError) {
|
|
368
|
+
console.log(error.status, error.responseBody);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
| Error | Meaning |
|
|
374
|
+
| --- | --- |
|
|
375
|
+
| `ApiError` | Non-2xx HTTP response after retries are exhausted. |
|
|
376
|
+
| `RateLimitError` | HTTP 429 response. Includes `retryAfterMs` when available. |
|
|
377
|
+
| `TimeoutError` | Request exceeded timeout. |
|
|
378
|
+
| `GraphQLRequestError` | GraphQL response contained an `errors` array. |
|
|
379
|
+
|
|
380
|
+
## Testing
|
|
381
|
+
|
|
382
|
+
Use a custom transport for deterministic tests:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
import { BaseHttpClient } from "@api-wrappers/api-core";
|
|
386
|
+
|
|
387
|
+
const client = new BaseHttpClient({
|
|
388
|
+
baseUrl: "https://api.example.com",
|
|
389
|
+
transport: {
|
|
390
|
+
execute: async (ctx) =>
|
|
391
|
+
new Response(JSON.stringify({ url: ctx.url }), {
|
|
392
|
+
headers: { "content-type": "application/json" },
|
|
393
|
+
}),
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
This exercises the client, request options, plugins, and error handling without
|
|
399
|
+
making network calls.
|
|
400
|
+
|
|
401
|
+
## Package Exports
|
|
402
|
+
|
|
403
|
+
```ts
|
|
404
|
+
import {
|
|
405
|
+
ApiError,
|
|
406
|
+
BaseHttpClient,
|
|
407
|
+
createAuthPlugin,
|
|
408
|
+
createCachePlugin,
|
|
409
|
+
createClient,
|
|
410
|
+
createLoggerPlugin,
|
|
411
|
+
createRateLimitPlugin,
|
|
412
|
+
createRetryPlugin,
|
|
413
|
+
createTimeoutPlugin,
|
|
414
|
+
GraphQLRequestError,
|
|
415
|
+
MemoryStore,
|
|
416
|
+
RateLimitError,
|
|
417
|
+
TimeoutError,
|
|
418
|
+
} from "@api-wrappers/api-core";
|
|
419
|
+
|
|
420
|
+
import type {
|
|
421
|
+
ApiPlugin,
|
|
422
|
+
ApiResponse,
|
|
423
|
+
ClientConfig,
|
|
424
|
+
QueryParams,
|
|
425
|
+
RequestContext,
|
|
426
|
+
RequestOptions,
|
|
427
|
+
ResponseContext,
|
|
428
|
+
Transport,
|
|
429
|
+
} from "@api-wrappers/api-core";
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
The package publishes:
|
|
433
|
+
|
|
434
|
+
- ESM: `dist/index.mjs`
|
|
435
|
+
- CommonJS: `dist/index.cjs`
|
|
436
|
+
- Type declarations for both module formats
|
|
437
|
+
- README and docs
|
|
438
|
+
|
|
439
|
+
## More Documentation
|
|
440
|
+
|
|
441
|
+
- [Documentation home](docs/README.md): recommended reading order and full docs
|
|
442
|
+
map.
|
|
443
|
+
- [Getting started](docs/getting-started.md): install, create a client, and make
|
|
444
|
+
the first request.
|
|
445
|
+
- [REST requests](docs/guides/rest-requests.md): methods, query params, request
|
|
446
|
+
bodies, abort signals, and response metadata.
|
|
447
|
+
- [Built-in plugins](docs/guides/built-in-plugins.md): auth, retry, timeout,
|
|
448
|
+
rate-limit, cache, and logger usage.
|
|
449
|
+
- [Client API reference](docs/reference/client.md): client methods and response
|
|
450
|
+
shapes.
|
|
451
|
+
|
|
452
|
+
## Development
|
|
453
|
+
|
|
454
|
+
```bash
|
|
455
|
+
bun install
|
|
456
|
+
bun run check
|
|
457
|
+
bun test
|
|
458
|
+
bun run build
|
|
459
|
+
npm pack --dry-run
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
`dist` is generated by `tsdown`. The published package includes `dist`, `docs`,
|
|
463
|
+
`README.md`, and `package.json`.
|