@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
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# REST Requests
|
|
2
|
+
|
|
3
|
+
The client exposes convenience methods for common HTTP verbs and a generic
|
|
4
|
+
`request` method for custom cases.
|
|
5
|
+
|
|
6
|
+
## Methods
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
client.get<T>(path, options);
|
|
10
|
+
client.post<T>(path, body, options);
|
|
11
|
+
client.put<T>(path, body, options);
|
|
12
|
+
client.patch<T>(path, body, options);
|
|
13
|
+
client.delete<T>(path, options);
|
|
14
|
+
client.head<T>(path, options);
|
|
15
|
+
client.options<T>(path, options);
|
|
16
|
+
client.request<T>(path, { method: "POST", body });
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`path` may start with `/` or omit it. Both are joined safely with `baseUrl`.
|
|
20
|
+
Absolute URLs are also supported.
|
|
21
|
+
|
|
22
|
+
## Request Options
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
await client.get("/movies", {
|
|
26
|
+
query: {
|
|
27
|
+
page: 1,
|
|
28
|
+
language: "en-US",
|
|
29
|
+
with_genres: [28, 12],
|
|
30
|
+
ignored: undefined,
|
|
31
|
+
},
|
|
32
|
+
headers: {
|
|
33
|
+
accept: "application/json",
|
|
34
|
+
},
|
|
35
|
+
timeoutMs: 10_000,
|
|
36
|
+
signal: abortController.signal,
|
|
37
|
+
cacheKey: "movies:page:1",
|
|
38
|
+
tags: ["movies"],
|
|
39
|
+
});
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
| Option | Description |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| `method` | HTTP method for `request`. Convenience methods set this for you. |
|
|
45
|
+
| `headers` | Per-request headers. These override `defaultHeaders`. |
|
|
46
|
+
| `body` | Request body. Objects/arrays are JSON encoded; strings are sent as-is. |
|
|
47
|
+
| `query` | Query string values. Arrays become repeated query parameters. |
|
|
48
|
+
| `timeoutMs` | Per-request timeout override. |
|
|
49
|
+
| `signal` | Caller-provided abort signal. Composes with timeout handling. |
|
|
50
|
+
| `cacheKey` | Explicit key for cache plugins. |
|
|
51
|
+
| `tags` | Labels plugins can use for cache invalidation or metrics. |
|
|
52
|
+
|
|
53
|
+
## Response Metadata
|
|
54
|
+
|
|
55
|
+
Use `requestWithResponse` when the wrapper needs headers, status, request data,
|
|
56
|
+
or plugin metadata.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
const result = await client.requestWithResponse<MoviePage>("/movie/popular");
|
|
60
|
+
|
|
61
|
+
result.data;
|
|
62
|
+
result.response.status;
|
|
63
|
+
result.response.headers.get("x-ratelimit-remaining");
|
|
64
|
+
result.request.url;
|
|
65
|
+
result.meta["cache.served"];
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`requestWithResponse` has the same retry, timeout, parsing, plugin, and error
|
|
69
|
+
behavior as `request`.
|
|
70
|
+
|
|
71
|
+
## Text Body APIs
|
|
72
|
+
|
|
73
|
+
Some APIs expect a custom text query language rather than JSON. Pass a string
|
|
74
|
+
body and override `content-type`.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
const games = await client.post<Game[]>(
|
|
78
|
+
"/games",
|
|
79
|
+
"fields name,rating; sort rating desc; limit 10;",
|
|
80
|
+
{
|
|
81
|
+
headers: {
|
|
82
|
+
"content-type": "text/plain",
|
|
83
|
+
accept: "application/json",
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Custom Fetch Or Transport
|
|
90
|
+
|
|
91
|
+
Use `fetch` when you only need to swap the fetch implementation:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const client = createClient({
|
|
95
|
+
baseUrl: "https://api.example.com",
|
|
96
|
+
fetch: customFetch,
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Use `transport` when you want full control over execution:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const client = createClient({
|
|
104
|
+
baseUrl: "https://api.example.com",
|
|
105
|
+
transport: {
|
|
106
|
+
execute: async (ctx) => {
|
|
107
|
+
return new Response(JSON.stringify({ url: ctx.url }), {
|
|
108
|
+
headers: { "content-type": "application/json" },
|
|
109
|
+
});
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
```
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Testing
|
|
2
|
+
|
|
3
|
+
Use a custom transport to test wrapper behavior without network calls.
|
|
4
|
+
|
|
5
|
+
## Basic Mock Transport
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import { BaseHttpClient } from "@api-wrappers/api-core";
|
|
9
|
+
|
|
10
|
+
const client = new BaseHttpClient({
|
|
11
|
+
baseUrl: "https://api.example.com",
|
|
12
|
+
transport: {
|
|
13
|
+
execute: async (ctx) =>
|
|
14
|
+
new Response(JSON.stringify({ url: ctx.url, method: ctx.method }), {
|
|
15
|
+
headers: { "content-type": "application/json" },
|
|
16
|
+
}),
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Assert Request Shape
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
let capturedUrl: string | undefined;
|
|
25
|
+
let capturedHeaders: Record<string, string> | undefined;
|
|
26
|
+
|
|
27
|
+
const client = new BaseHttpClient({
|
|
28
|
+
baseUrl: "https://api.example.com",
|
|
29
|
+
defaultHeaders: { accept: "application/json" },
|
|
30
|
+
transport: {
|
|
31
|
+
execute: async (ctx) => {
|
|
32
|
+
capturedUrl = ctx.url;
|
|
33
|
+
capturedHeaders = ctx.headers;
|
|
34
|
+
return new Response(JSON.stringify({ ok: true }), {
|
|
35
|
+
headers: { "content-type": "application/json" },
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
await client.get("/users/1");
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Test Errors
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const client = new BaseHttpClient({
|
|
48
|
+
baseUrl: "https://api.example.com",
|
|
49
|
+
transport: {
|
|
50
|
+
execute: async () =>
|
|
51
|
+
new Response(JSON.stringify({ message: "not found" }), {
|
|
52
|
+
status: 404,
|
|
53
|
+
headers: { "content-type": "application/json" },
|
|
54
|
+
}),
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await expect(client.get("/missing")).rejects.toThrow();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Test Plugins
|
|
62
|
+
|
|
63
|
+
Register the same plugins your wrapper uses in production. The transport stays
|
|
64
|
+
mocked, but auth, cache, retry, timeout, and custom plugin behavior still runs.
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const cache = createCachePlugin({ ttlMs: 60_000 });
|
|
68
|
+
|
|
69
|
+
const client = new BaseHttpClient({
|
|
70
|
+
baseUrl: "https://api.example.com",
|
|
71
|
+
plugins: [createAuthPlugin("token"), cache],
|
|
72
|
+
transport: {
|
|
73
|
+
execute: async () =>
|
|
74
|
+
new Response(JSON.stringify({ ok: true }), {
|
|
75
|
+
headers: { "content-type": "application/json" },
|
|
76
|
+
}),
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Smoke Test A Built Package
|
|
82
|
+
|
|
83
|
+
After `bun run build`, verify both module formats:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
node --input-type=module -e "const m = await import('./dist/index.mjs'); if (!m.createClient) throw new Error('missing ESM export')"
|
|
87
|
+
node -e "const m = require('./dist/index.cjs'); if (!m.createClient) throw new Error('missing CJS export')"
|
|
88
|
+
```
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Client API
|
|
2
|
+
|
|
3
|
+
## `createClient(config)`
|
|
4
|
+
|
|
5
|
+
Creates a `BaseHttpClient`.
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
const client = createClient({
|
|
9
|
+
baseUrl: "https://api.example.com",
|
|
10
|
+
});
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## `BaseHttpClient`
|
|
14
|
+
|
|
15
|
+
Use `BaseHttpClient` directly when building wrapper-specific classes.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
class MyApiClient extends BaseHttpClient {
|
|
19
|
+
getUser(id: string) {
|
|
20
|
+
return this.get<User>(`/users/${id}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Methods
|
|
26
|
+
|
|
27
|
+
| Method | Description |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| `request<T>(path, options)` | Execute a request and return the parsed body. |
|
|
30
|
+
| `requestWithResponse<T>(path, options)` | Execute a request and return body, response, request context, and metadata. |
|
|
31
|
+
| `get<T>(path, options)` | GET request. |
|
|
32
|
+
| `post<T>(path, body, options)` | POST request. |
|
|
33
|
+
| `put<T>(path, body, options)` | PUT request. |
|
|
34
|
+
| `patch<T>(path, body, options)` | PATCH request. |
|
|
35
|
+
| `delete<T>(path, options)` | DELETE request. |
|
|
36
|
+
| `head<T>(path, options)` | HEAD request. |
|
|
37
|
+
| `options<T>(path, options)` | OPTIONS request. |
|
|
38
|
+
| `graphql<TData, TVariables>(path, options)` | GraphQL POST request returning `data`. |
|
|
39
|
+
| `init()` | Initialize plugins. Called lazily before the first request. |
|
|
40
|
+
| `dispose()` | Run plugin cleanup hooks. |
|
|
41
|
+
|
|
42
|
+
## `ApiResponse<T>`
|
|
43
|
+
|
|
44
|
+
Returned by `requestWithResponse`.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface ApiResponse<T = unknown> {
|
|
48
|
+
data: T;
|
|
49
|
+
response: Response;
|
|
50
|
+
request: RequestContext;
|
|
51
|
+
meta: Record<string, unknown>;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Configuration Reference
|
|
2
|
+
|
|
3
|
+
## `ClientConfig`
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
interface ClientConfig {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
defaultHeaders?: Record<string, string>;
|
|
9
|
+
plugins?: ApiPlugin[];
|
|
10
|
+
transport?: Transport;
|
|
11
|
+
fetch?: typeof globalThis.fetch;
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
retry?: RetryConfig;
|
|
14
|
+
logger?: LoggerInterface;
|
|
15
|
+
}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Field | Description |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| `baseUrl` | Base URL prepended to relative request paths. |
|
|
21
|
+
| `defaultHeaders` | Headers merged into every request. |
|
|
22
|
+
| `plugins` | Plugins registered for the client lifetime. |
|
|
23
|
+
| `transport` | Custom request executor. Takes precedence over `fetch`. |
|
|
24
|
+
| `fetch` | Custom fetch implementation used by the default transport. |
|
|
25
|
+
| `timeoutMs` | Default timeout for every request. |
|
|
26
|
+
| `retry` | Global retry policy. |
|
|
27
|
+
| `logger` | Logger used for internal diagnostics. |
|
|
28
|
+
|
|
29
|
+
## `RequestOptions`
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
interface RequestOptions {
|
|
33
|
+
method?: HttpMethod;
|
|
34
|
+
headers?: Record<string, string>;
|
|
35
|
+
body?: unknown;
|
|
36
|
+
query?: QueryParams;
|
|
37
|
+
signal?: AbortSignal;
|
|
38
|
+
timeoutMs?: number;
|
|
39
|
+
cacheKey?: string;
|
|
40
|
+
tags?: string[];
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## `RetryConfig`
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
interface RetryConfig {
|
|
48
|
+
maxAttempts: number;
|
|
49
|
+
delayMs?: number;
|
|
50
|
+
jitter?: boolean;
|
|
51
|
+
retriableStatusCodes?: number[];
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`maxAttempts` includes the first attempt. A value of `1` means no retry.
|
|
56
|
+
|
|
57
|
+
## Query Types
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type QueryPrimitive = string | number | boolean;
|
|
61
|
+
|
|
62
|
+
type QueryValue =
|
|
63
|
+
| QueryPrimitive
|
|
64
|
+
| null
|
|
65
|
+
| undefined
|
|
66
|
+
| readonly (QueryPrimitive | null | undefined)[];
|
|
67
|
+
|
|
68
|
+
type QueryParams = Record<string, QueryValue>;
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Nullish query values are skipped. Array query values are emitted as repeated
|
|
72
|
+
query parameters.
|
|
73
|
+
|
|
74
|
+
## `Transport`
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
interface Transport {
|
|
78
|
+
execute(ctx: RequestContext): Promise<Response>;
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Custom transports are useful for tests, tracing, custom networking layers, or
|
|
83
|
+
non-fetch runtimes.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Exports
|
|
2
|
+
|
|
3
|
+
## Client
|
|
4
|
+
|
|
5
|
+
- `createClient`
|
|
6
|
+
- `BaseHttpClient`
|
|
7
|
+
- `RequestOptions`
|
|
8
|
+
- `ApiResponse`
|
|
9
|
+
- `ClientConfig`
|
|
10
|
+
- `LoggerInterface`
|
|
11
|
+
- `RetryConfig`
|
|
12
|
+
|
|
13
|
+
## Context
|
|
14
|
+
|
|
15
|
+
- `RequestContext`
|
|
16
|
+
- `ResponseContext`
|
|
17
|
+
|
|
18
|
+
## Errors
|
|
19
|
+
|
|
20
|
+
- `ApiError`
|
|
21
|
+
- `RateLimitError`
|
|
22
|
+
- `TimeoutError`
|
|
23
|
+
- `GraphQLRequestError`
|
|
24
|
+
|
|
25
|
+
## Plugin System
|
|
26
|
+
|
|
27
|
+
- `ApiPlugin`
|
|
28
|
+
- `PluginManager`
|
|
29
|
+
|
|
30
|
+
## Built-In Plugins
|
|
31
|
+
|
|
32
|
+
- `createAuthPlugin`
|
|
33
|
+
- `AuthPluginOptions`
|
|
34
|
+
- `createCachePlugin`
|
|
35
|
+
- `CachePlugin`
|
|
36
|
+
- `CachePluginOptions`
|
|
37
|
+
- `CacheStore`
|
|
38
|
+
- `MemoryStore`
|
|
39
|
+
- `createLoggerPlugin`
|
|
40
|
+
- `LoggerPluginOptions`
|
|
41
|
+
- `createRateLimitPlugin`
|
|
42
|
+
- `RateLimitPluginOptions`
|
|
43
|
+
- `createRetryPlugin`
|
|
44
|
+
- `RetryPluginOptions`
|
|
45
|
+
- `createTimeoutPlugin`
|
|
46
|
+
- `TimeoutPluginOptions`
|
|
47
|
+
|
|
48
|
+
## GraphQL
|
|
49
|
+
|
|
50
|
+
- `GraphQLErrorDetail`
|
|
51
|
+
- `GraphQLRequestOptions`
|
|
52
|
+
- `GraphQLResponse`
|
|
53
|
+
|
|
54
|
+
## Transport
|
|
55
|
+
|
|
56
|
+
- `Transport`
|
|
57
|
+
- `createFetchTransport`
|
|
58
|
+
- `fetchTransport`
|
|
59
|
+
|
|
60
|
+
## Shared Types
|
|
61
|
+
|
|
62
|
+
- `HttpMethod`
|
|
63
|
+
- `MaybePromise`
|
|
64
|
+
- `QueryParams`
|
|
65
|
+
- `QueryPrimitive`
|
|
66
|
+
- `QueryValue`
|
|
67
|
+
|
|
68
|
+
## Utilities
|
|
69
|
+
|
|
70
|
+
- `buildUrl`
|
|
71
|
+
- `isPlainObject`
|
|
72
|
+
- `mergeHeaders`
|
|
73
|
+
- `resolveUrl`
|
|
74
|
+
- `sleep`
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@api-wrappers/api-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Shared HTTP client runtime for the api-wrappers organisation. Provides request orchestration, a plugin lifecycle, transport abstraction, and built-in cache/retry/logger plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.cjs",
|
|
7
|
+
"module": "./dist/index.mjs",
|
|
8
|
+
"types": "./dist/index.d.mts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/index.d.mts",
|
|
13
|
+
"default": "./dist/index.mjs"
|
|
14
|
+
},
|
|
15
|
+
"require": {
|
|
16
|
+
"types": "./dist/index.d.cts",
|
|
17
|
+
"default": "./dist/index.cjs"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"docs"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"http",
|
|
27
|
+
"client",
|
|
28
|
+
"fetch",
|
|
29
|
+
"plugin",
|
|
30
|
+
"cache",
|
|
31
|
+
"retry",
|
|
32
|
+
"api",
|
|
33
|
+
"wrapper",
|
|
34
|
+
"typescript"
|
|
35
|
+
],
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsdown",
|
|
39
|
+
"test": "bun test",
|
|
40
|
+
"check": "biome check --write .",
|
|
41
|
+
"check:ci": "biome check ."
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@biomejs/biome": "2.4.9",
|
|
48
|
+
"@types/bun": "1.3.11",
|
|
49
|
+
"tsdown": "0.21.7"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"typescript": "^5"
|
|
53
|
+
}
|
|
54
|
+
}
|