@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 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`.