@hardlydifficult/rest-client 1.0.3 → 1.0.4
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 +361 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# @hardlydifficult/rest-client
|
|
2
|
+
|
|
3
|
+
A typed REST client library with declarative operation definitions, OAuth2/bearer/custom token authentication, automatic retry logic, Zod-based param validation, and structured error handling.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @hardlydifficult/rest-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { RestClient, defineOperation } from "@hardlydifficult/rest-client";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
// Define a REST operation
|
|
18
|
+
const GetUser = defineOperation<{ id: string }, User>({
|
|
19
|
+
params: z.object({ id: z.string() }),
|
|
20
|
+
method: "GET",
|
|
21
|
+
url: (p, base) => `${base}/users/${p.id}`,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Create a client with OAuth2 auth
|
|
25
|
+
class MyApi extends RestClient {
|
|
26
|
+
getUser = this.bind(GetUser);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const api = new MyApi({
|
|
30
|
+
baseUrl: "https://api.example.com",
|
|
31
|
+
auth: {
|
|
32
|
+
type: "oauth2",
|
|
33
|
+
tokenUrl: "https://auth.example.com/token",
|
|
34
|
+
clientId: "client-id",
|
|
35
|
+
clientSecret: "secret",
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Make a request
|
|
40
|
+
const user = await api.getUser({ id: "123" });
|
|
41
|
+
// => User object with id, name, etc.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Core Classes
|
|
45
|
+
|
|
46
|
+
### RestClient
|
|
47
|
+
|
|
48
|
+
Opinionated REST client with OAuth2, retries, and declarative operations.
|
|
49
|
+
|
|
50
|
+
#### Constructor
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
new RestClient(config: RestClientConfig)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
| Property | Type | Description |
|
|
57
|
+
|----------|------|-------------|
|
|
58
|
+
| `baseUrl` | `string` | Base URL for all requests (required) |
|
|
59
|
+
| `auth` | `AuthConfig?` | Authentication configuration (optional, default: `{ type: "none" }`) |
|
|
60
|
+
| `retry` | `RetryConfig?` | Retry settings (optional, default: `{ maxAttempts: 3, delayMs: 6000 }`) |
|
|
61
|
+
| `defaultHeaders` | `Record<string, string>?` | Default HTTP headers (optional) |
|
|
62
|
+
| `logger` | `RestClientLogger?` | Logger instance (optional) |
|
|
63
|
+
|
|
64
|
+
#### Methods
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
bind<Params, Response>(
|
|
68
|
+
config: OperationConfig<Params, Response>
|
|
69
|
+
): (params: Params) => Promise<Response>
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Bind a declarative operation to the client.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
await authenticate(): Promise<string>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Authenticate and set the bearer token.
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
clearToken(): void
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Clear the cached token, forcing re-authentication on next request.
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
getBaseUrl(): string
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Get the base URL.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
getLogger(): RestClientLogger | undefined
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Get the logger instance.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
await get<T>(url: string): Promise<T>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Perform a GET request.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
await post<T>(url: string, data?: unknown): Promise<T>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Perform a POST request.
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
await delete<T>(url: string): Promise<T>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Perform a DELETE request.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
await patch<T>(url: string, data?: unknown): Promise<T>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Perform a PATCH request.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
await put<T>(url: string, data?: unknown): Promise<T>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Perform a PUT request.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
getTokenExpiryTime(): number | null
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Token expiry timestamp (ms since epoch), or `null` if unavailable.
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
getTokenIssuedAt(): number | null
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Token issue timestamp (ms since epoch), or `null` if unavailable.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
getTokenLifetimeMs(): number | null
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Token lifetime in milliseconds, or `null` if unavailable.
|
|
145
|
+
|
|
146
|
+
### HttpClient
|
|
147
|
+
|
|
148
|
+
HTTP client with automatic retries and structured error formatting.
|
|
149
|
+
|
|
150
|
+
#### Constructor
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
new HttpClient(options?: {
|
|
154
|
+
logger?: RestClientLogger;
|
|
155
|
+
retry?: RetryConfig;
|
|
156
|
+
defaultHeaders?: Record<string, string>;
|
|
157
|
+
})
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
#### Methods
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
setBearerToken(token: string): void
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Set the Authorization header with Bearer token.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
clearBearerToken(): void
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Clear the Authorization header.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
await get<T>(url: string): Promise<T>
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Perform a GET request.
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
await post<T>(url: string, data?: unknown): Promise<T>
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Perform a POST request.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
await delete<T>(url: string): Promise<T>
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Perform a DELETE request.
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
await patch<T>(url: string, data?: unknown): Promise<T>
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Perform a PATCH request.
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
await put<T>(url: string, data?: unknown): Promise<T>
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Perform a PUT request.
|
|
203
|
+
|
|
204
|
+
#### Retry Behavior
|
|
205
|
+
|
|
206
|
+
- Automatically retries on 5xx errors and network failures
|
|
207
|
+
- Default: 3 attempts with 6000ms delay between retries
|
|
208
|
+
- Custom retryable status codes or conditions可通过 `retryableStatuses` and `isRetryable`
|
|
209
|
+
|
|
210
|
+
### AuthenticationManager
|
|
211
|
+
|
|
212
|
+
Manages token lifecycle for all supported auth strategies.
|
|
213
|
+
|
|
214
|
+
#### Constructor
|
|
215
|
+
|
|
216
|
+
```typescript
|
|
217
|
+
new AuthenticationManager(config: AuthConfig, logger?: RestClientLogger)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
#### Methods
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
await authenticate(): Promise<string>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Authenticate and return the bearer token.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
clearToken(): void
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Clear cached token and timing info.
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
getTokenExpiryTime(): number | null
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Token expiry timestamp (ms since epoch), or `null` if unavailable.
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
getTokenIssuedAt(): number | null
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Token issue timestamp (ms since epoch), or `null` if unavailable.
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
getTokenLifetimeMs(): number | null
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Token lifetime in milliseconds, or `null` if unavailable.
|
|
251
|
+
|
|
252
|
+
#### Supported Auth Types
|
|
253
|
+
|
|
254
|
+
| Type | Config | Description |
|
|
255
|
+
|------|--------|-------------|
|
|
256
|
+
| `bearer` | `{ type: "bearer", token: string }` | Static bearer token |
|
|
257
|
+
| `generator` | `{ type: "generator", generate: () => Promise<string>, cacheDurationMs?: number }` | Async token generator with optional cache |
|
|
258
|
+
| `oauth2` | `{ type: "oauth2", tokenUrl: string, clientId: string, clientSecret?: string, audience?: string, scope?: string, grantType?: "client_credentials" \| "password", username?: string, password?: string }` | OAuth2 client credentials or password grant |
|
|
259
|
+
| `none` | `{ type: "none" }` | No authentication (default) |
|
|
260
|
+
|
|
261
|
+
### defineOperation
|
|
262
|
+
|
|
263
|
+
Define a REST operation declaratively.
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
const GetUser = defineOperation<{ id: string }, User>({
|
|
267
|
+
params: z.object({ id: z.string() }),
|
|
268
|
+
method: "GET",
|
|
269
|
+
url: (p, base) => `${base}/users/${p.id}`,
|
|
270
|
+
});
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
#### Config Fields
|
|
274
|
+
|
|
275
|
+
| Field | Type | Required | Description |
|
|
276
|
+
|-------|------|----------|-------------|
|
|
277
|
+
| `params` | `z.ZodType<Params>` | Yes | Zod schema for request parameters |
|
|
278
|
+
| `method` | `HttpMethod` | Yes | HTTP method: `"GET"`, `"POST"`, `"DELETE"`, `"PATCH"`, `"PUT"` |
|
|
279
|
+
| `url` | `(params: Params, baseUrl: string) => string` | Yes | URL builder function |
|
|
280
|
+
| `body` | `(params: Params) => Record<string, unknown> \| string \| undefined` | No | Optional body builder |
|
|
281
|
+
| `transform` | `(response: Response) => Response` | No | Optional response transformer |
|
|
282
|
+
|
|
283
|
+
### validateParams
|
|
284
|
+
|
|
285
|
+
Validate params against a Zod schema, throwing `ValidationError` on failure.
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
validateParams<T>(params: T, schema: z.ZodType<T>): T
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
const validated = validateParams({ id: "123" }, z.object({ id: z.string() }));
|
|
293
|
+
// => { id: "123" }
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
## Error Handling
|
|
297
|
+
|
|
298
|
+
All errors extend `RestClientError` and include a `code` and optional `context`.
|
|
299
|
+
|
|
300
|
+
| Error Type | Code | Description |
|
|
301
|
+
|------------|------|-------------|
|
|
302
|
+
| `ConfigurationError` | `"CONFIGURATION_ERROR"` | Invalid client configuration (e.g., missing `baseUrl`) |
|
|
303
|
+
| `AuthenticationError` | `"AUTHENTICATION_ERROR"` | Authentication failure |
|
|
304
|
+
| `HttpError` | `"HTTP_ERROR"` | HTTP error with status, statusText, and response data |
|
|
305
|
+
| `ValidationError` | `"VALIDATION_ERROR"` | Parameter validation failure (via Zod) |
|
|
306
|
+
| `NetworkError` | `"NETWORK_ERROR"` | Network-level errors (e.g., DNS failure, connection refused) |
|
|
307
|
+
|
|
308
|
+
Example:
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
try {
|
|
312
|
+
await api.getUser({ id: "123" });
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error instanceof HttpError) {
|
|
315
|
+
console.log(error.status, error.message);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
## Types
|
|
321
|
+
|
|
322
|
+
### AuthConfig
|
|
323
|
+
|
|
324
|
+
Union type of auth configurations: `OAuth2Auth | BearerAuth | TokenGeneratorAuth | NoAuth`
|
|
325
|
+
|
|
326
|
+
### RetryConfig
|
|
327
|
+
|
|
328
|
+
| Property | Type | Default | Description |
|
|
329
|
+
|----------|------|---------|-------------|
|
|
330
|
+
| `maxAttempts` | `number` | `3` | Number of retries after the initial attempt |
|
|
331
|
+
| `delayMs` | `number` | `6000` | Delay between retries in milliseconds |
|
|
332
|
+
| `retryableStatuses` | `readonly number[]?` | `undefined` | Additional HTTP status codes to retry on |
|
|
333
|
+
| `isRetryable` | `(status: number, body: Record<string, unknown>) => boolean?` | `undefined` | Custom retryable check |
|
|
334
|
+
|
|
335
|
+
### RestClientLogger
|
|
336
|
+
|
|
337
|
+
Minimal logger interface (compatible with `@hardlydifficult/logger`):
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
interface RestClientLogger {
|
|
341
|
+
debug?(message: string, context?: Record<string, unknown>): void;
|
|
342
|
+
info?(message: string, context?: Record<string, unknown>): void;
|
|
343
|
+
warn?(message: string, context?: Record<string, unknown>): void;
|
|
344
|
+
error?(message: string, context?: Record<string, unknown>): void;
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
All methods are optional — only implement what you need.
|
|
349
|
+
|
|
350
|
+
## Appendix
|
|
351
|
+
|
|
352
|
+
### Retryable Status Codes
|
|
353
|
+
|
|
354
|
+
By default, the client retries on:
|
|
355
|
+
|
|
356
|
+
- All 5xx status codes (500–599)
|
|
357
|
+
- Network errors (no response)
|
|
358
|
+
- Custom status codes via `retryableStatuses`
|
|
359
|
+
- Custom conditions via `isRetryable`
|
|
360
|
+
|
|
361
|
+
OAuth2 token refresh is automatically triggered when the token is near expiry (half its lifetime or 5 minutes, whichever is smaller).
|