@igniter-js/caller 0.1.56 → 0.1.58
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 +1129 -412
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +15 -4
- package/dist/index.mjs.map +1 -1
- package/dist/{manager-DQcuAp3y.d.ts → manager-1P9h5bga.d.ts} +1 -1
- package/dist/{manager-DXmJbnQY.d.mts → manager-DxYY2G8O.d.mts} +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,619 +1,1336 @@
|
|
|
1
1
|
# @igniter-js/caller
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
<div align="center">
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@igniter-js/caller)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
[](https://nodejs.org/)
|
|
9
|
+
[](https://bun.sh)
|
|
7
10
|
|
|
8
|
-
|
|
11
|
+
**End-to-end type-safe HTTP client**
|
|
12
|
+
Built on `fetch` with interceptors, retries, caching, schema validation, and full observability.
|
|
9
13
|
|
|
10
|
-
-
|
|
11
|
-
- ✅ **axios-style requests** - `api.request({ method, url, body, ... })`
|
|
12
|
-
- ✅ **Auto content-type detection** - JSON, XML, CSV, Blob, Stream, etc.
|
|
13
|
-
- ✅ **Interceptors** - modify requests and responses in one place
|
|
14
|
-
- ✅ **Retries** - linear or exponential backoff + status-based retry
|
|
15
|
-
- ✅ **Caching** - in-memory cache + optional persistent store adapter
|
|
16
|
-
- ✅ **Schema Validation** - validate request/response using `StandardSchemaV1`
|
|
17
|
-
- ✅ **StandardSchema Support** - Zod or any library that implements `StandardSchemaV1`
|
|
18
|
-
- ✅ **Telemetry-ready** - optional integration with `@igniter-js/telemetry`
|
|
19
|
-
- ✅ **Global Events** - observe responses for logging/telemetry/cache invalidation
|
|
20
|
-
- ✅ **Auto query encoding** - body in GET requests converts to query params
|
|
14
|
+
[Quick Start](#-quick-start) • [Documentation](https://igniterjs.com/docs/caller) • [Examples](#-real-world-examples) • [API Reference](#-api-reference)
|
|
21
15
|
|
|
22
|
-
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ✨ Why @igniter-js/caller?
|
|
21
|
+
|
|
22
|
+
Making API calls shouldn't require choosing between developer experience and runtime safety. Whether you're building a SaaS platform, a mobile backend, or a microservices architecture, you need:
|
|
23
|
+
|
|
24
|
+
- ✅ **End-to-end type safety** — Catch API mismatches at build time, not in production
|
|
25
|
+
- ✅ **Zero configuration** — Works anywhere `fetch` works (Node 18+, Bun, Deno, browsers)
|
|
26
|
+
- ✅ **Production resilience** — Retries, timeouts, fallbacks, and caching built-in
|
|
27
|
+
- ✅ **Full observability** — Telemetry, logging, and global events for every request
|
|
28
|
+
- ✅ **Schema validation** — Runtime type checking with Zod, Valibot, or any StandardSchemaV1 library
|
|
29
|
+
- ✅ **Developer experience** — Fluent API, autocomplete everywhere, zero boilerplate
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🚀 Quick Start
|
|
34
|
+
|
|
35
|
+
### Installation
|
|
23
36
|
|
|
24
37
|
```bash
|
|
25
|
-
# npm
|
|
26
|
-
npm install @igniter-js/caller
|
|
38
|
+
# Using npm
|
|
39
|
+
npm install @igniter-js/caller
|
|
27
40
|
|
|
28
|
-
# pnpm
|
|
29
|
-
pnpm add @igniter-js/caller
|
|
41
|
+
# Using pnpm
|
|
42
|
+
pnpm add @igniter-js/caller
|
|
30
43
|
|
|
31
|
-
# yarn
|
|
32
|
-
yarn add @igniter-js/caller
|
|
44
|
+
# Using yarn
|
|
45
|
+
yarn add @igniter-js/caller
|
|
33
46
|
|
|
34
|
-
# bun
|
|
35
|
-
bun add @igniter-js/caller
|
|
47
|
+
# Using bun
|
|
48
|
+
bun add @igniter-js/caller
|
|
36
49
|
```
|
|
37
50
|
|
|
38
|
-
Optional dependencies
|
|
51
|
+
**Optional dependencies:**
|
|
39
52
|
|
|
40
53
|
```bash
|
|
41
|
-
#
|
|
54
|
+
# For schema validation (any StandardSchemaV1 library)
|
|
55
|
+
npm install zod
|
|
56
|
+
|
|
57
|
+
# For observability (optional)
|
|
42
58
|
npm install @igniter-js/telemetry
|
|
59
|
+
```
|
|
43
60
|
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
> **Note:** `@igniter-js/common` is automatically installed as a dependency. `zod` and `@igniter-js/telemetry` are optional peer dependencies.
|
|
62
|
+
|
|
63
|
+
### Your First API Call (60 seconds)
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
67
|
+
|
|
68
|
+
// 1️⃣ Create the client
|
|
69
|
+
const api = IgniterCaller.create()
|
|
70
|
+
.withBaseUrl('https://api.github.com')
|
|
71
|
+
.withHeaders({
|
|
72
|
+
'Accept': 'application/vnd.github+json',
|
|
73
|
+
'X-GitHub-Api-Version': '2022-11-28'
|
|
74
|
+
})
|
|
75
|
+
.build();
|
|
76
|
+
|
|
77
|
+
// 2️⃣ Make a request
|
|
78
|
+
const result = await api.get('/users/octocat').execute();
|
|
79
|
+
|
|
80
|
+
// 3️⃣ Handle the response
|
|
81
|
+
if (result.error) {
|
|
82
|
+
console.error('Request failed:', result.error.message);
|
|
83
|
+
} else {
|
|
84
|
+
console.log('User:', result.data);
|
|
85
|
+
}
|
|
46
86
|
```
|
|
47
87
|
|
|
48
|
-
|
|
88
|
+
**✅ Success!** You just made a type-safe HTTP request with zero configuration.
|
|
49
89
|
|
|
50
|
-
|
|
90
|
+
---
|
|
51
91
|
|
|
52
|
-
|
|
53
|
-
import { IgniterCaller } from '@igniter-js/caller'
|
|
92
|
+
## 🎯 Core Concepts
|
|
54
93
|
|
|
55
|
-
|
|
94
|
+
### Architecture Overview
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
98
|
+
│ Your Application │
|
|
99
|
+
├──────────────────────────────────────────────────────────────────┤
|
|
100
|
+
│ api.get('/users').params({ page: 1 }).execute() │
|
|
101
|
+
└────────────┬─────────────────────────────────────────────────────┘
|
|
102
|
+
│ Type-safe fluent API
|
|
103
|
+
▼
|
|
104
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
105
|
+
│ IgniterCallerBuilder (Immutable) │
|
|
106
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
107
|
+
│ │ Configuration: │ │
|
|
108
|
+
│ │ - baseURL, headers, cookies │ │
|
|
109
|
+
│ │ - requestInterceptors, responseInterceptors │ │
|
|
110
|
+
│ │ - store, schemas, telemetry, logger │ │
|
|
111
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
112
|
+
└────────────┬─────────────────────────────────────────────────────┘
|
|
113
|
+
│ .build()
|
|
114
|
+
▼
|
|
115
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
116
|
+
│ IgniterCallerManager (Runtime) │
|
|
117
|
+
│ - get/post/put/patch/delete/head() → RequestBuilder │
|
|
118
|
+
│ - request() → axios-style direct execution │
|
|
119
|
+
│ - Static: batch(), on(), invalidate() │
|
|
120
|
+
└────────────┬─────────────────────────────────────────────────────┘
|
|
121
|
+
│ Creates
|
|
122
|
+
▼
|
|
123
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
124
|
+
│ IgniterCallerRequestBuilder (Per-Request) │
|
|
125
|
+
│ ┌────────────────────────────────────────────────────────────┐ │
|
|
126
|
+
│ │ Configuration: │ │
|
|
127
|
+
│ │ - url, method, body, params, headers │ │
|
|
128
|
+
│ │ - timeout, cache, staleTime, retry │ │
|
|
129
|
+
│ │ - fallback, responseType (schema or type marker) │ │
|
|
130
|
+
│ └────────────────────────────────────────────────────────────┘ │
|
|
131
|
+
└────────────┬─────────────────────────────────────────────────────┘
|
|
132
|
+
│ .execute()
|
|
133
|
+
▼
|
|
134
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
135
|
+
│ Execution Pipeline │
|
|
136
|
+
│ 1. Cache Check (if staleTime set) │
|
|
137
|
+
│ 2. Request Interceptors │
|
|
138
|
+
│ 3. Request Validation (if schema defined) │
|
|
139
|
+
│ 4. Fetch with Retry Logic │
|
|
140
|
+
│ 5. Response Parsing (Content-Type auto-detect) │
|
|
141
|
+
│ 6. Response Validation (if schema defined) │
|
|
142
|
+
│ 7. Response Interceptors │
|
|
143
|
+
│ 8. Cache Store (if successful) │
|
|
144
|
+
│ 9. Fallback (if failed and fallback set) │
|
|
145
|
+
│ 10. Telemetry Emission │
|
|
146
|
+
│ 11. Global Event Emission │
|
|
147
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Key Abstractions
|
|
151
|
+
|
|
152
|
+
- **Builder** → Immutable configuration (`.withHeaders()`, `.withSchemas()`)
|
|
153
|
+
- **Manager** → Operational HTTP client instance (`.get()`, `.post()`)
|
|
154
|
+
- **RequestBuilder** → Per-request fluent API (`.body()`, `.retry()`, `.execute()`)
|
|
155
|
+
- **Interceptors** → Request/Response transformation pipeline
|
|
156
|
+
- **Schemas** → Type inference + runtime validation (StandardSchemaV1)
|
|
157
|
+
- **Cache** → In-memory or store-based (Redis, etc.)
|
|
158
|
+
- **Events** → Global observation for logging/telemetry
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 📖 Usage Examples
|
|
163
|
+
|
|
164
|
+
### Basic Usage
|
|
165
|
+
|
|
166
|
+
```typescript
|
|
167
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
168
|
+
|
|
169
|
+
const api = IgniterCaller.create()
|
|
56
170
|
.withBaseUrl('https://api.example.com')
|
|
57
171
|
.withHeaders({ Authorization: `Bearer ${process.env.API_TOKEN}` })
|
|
58
|
-
.build()
|
|
172
|
+
.build();
|
|
173
|
+
|
|
174
|
+
// GET request
|
|
175
|
+
const users = await api.get('/users').execute();
|
|
176
|
+
|
|
177
|
+
// POST request with body
|
|
178
|
+
const newUser = await api
|
|
179
|
+
.post('/users')
|
|
180
|
+
.body({ name: 'John Doe', email: 'john@example.com' })
|
|
181
|
+
.execute();
|
|
182
|
+
|
|
183
|
+
// PUT request with params
|
|
184
|
+
const updated = await api
|
|
185
|
+
.put('/users/:id')
|
|
186
|
+
.params({ id: '123' })
|
|
187
|
+
.body({ name: 'Jane Doe' })
|
|
188
|
+
.execute();
|
|
189
|
+
|
|
190
|
+
// DELETE request
|
|
191
|
+
const deleted = await api.delete('/users/123').execute();
|
|
192
|
+
|
|
193
|
+
// Check for errors
|
|
194
|
+
if (users.error) {
|
|
195
|
+
console.error('Request failed:', users.error.message);
|
|
196
|
+
throw users.error;
|
|
197
|
+
}
|
|
59
198
|
|
|
60
|
-
|
|
61
|
-
|
|
199
|
+
console.log('Users:', users.data);
|
|
200
|
+
```
|
|
62
201
|
|
|
63
|
-
|
|
64
|
-
const result = await api.get('/users').params({ page: 1 }).execute()
|
|
202
|
+
### Query Parameters
|
|
65
203
|
|
|
66
|
-
|
|
67
|
-
|
|
204
|
+
```typescript
|
|
205
|
+
// Using .params()
|
|
206
|
+
const result = await api
|
|
207
|
+
.get('/search')
|
|
208
|
+
.params({ q: 'typescript', page: 1, limit: 10 })
|
|
209
|
+
.execute();
|
|
68
210
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
211
|
+
// GET with body (auto-converted to query params)
|
|
212
|
+
const result = await api
|
|
213
|
+
.get('/search')
|
|
214
|
+
.body({ q: 'typescript', page: 1 })
|
|
215
|
+
.execute();
|
|
216
|
+
// Becomes: GET /search?q=typescript&page=1
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Request Headers
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// Per-request headers (merged with defaults)
|
|
223
|
+
const result = await api
|
|
224
|
+
.get('/users')
|
|
225
|
+
.headers({ 'X-Custom-Header': 'value' })
|
|
226
|
+
.execute();
|
|
72
227
|
|
|
73
|
-
|
|
228
|
+
// Override default headers
|
|
229
|
+
const result = await api
|
|
230
|
+
.get('/public/data')
|
|
231
|
+
.headers({ Authorization: '' }) // Remove auth for this request
|
|
232
|
+
.execute();
|
|
74
233
|
```
|
|
75
234
|
|
|
76
|
-
|
|
235
|
+
### Timeout & Retry
|
|
77
236
|
|
|
78
|
-
|
|
237
|
+
```typescript
|
|
238
|
+
// Set timeout
|
|
239
|
+
const result = await api
|
|
240
|
+
.get('/slow-endpoint')
|
|
241
|
+
.timeout(5000) // 5 seconds
|
|
242
|
+
.execute();
|
|
79
243
|
|
|
80
|
-
|
|
81
|
-
|
|
244
|
+
// Retry with exponential backoff
|
|
245
|
+
const result = await api
|
|
246
|
+
.get('/unreliable-endpoint')
|
|
247
|
+
.retry(3, {
|
|
248
|
+
baseDelay: 500,
|
|
249
|
+
backoff: 'exponential',
|
|
250
|
+
retryOnStatus: [408, 429, 500, 502, 503, 504],
|
|
251
|
+
})
|
|
252
|
+
.execute();
|
|
82
253
|
```
|
|
83
254
|
|
|
84
|
-
|
|
255
|
+
### Caching
|
|
85
256
|
|
|
86
|
-
|
|
257
|
+
```typescript
|
|
258
|
+
// In-memory cache
|
|
259
|
+
const result = await api
|
|
260
|
+
.get('/users')
|
|
261
|
+
.stale(60_000) // Cache for 60 seconds
|
|
262
|
+
.execute();
|
|
87
263
|
|
|
88
|
-
|
|
89
|
-
|
|
264
|
+
// Custom cache key
|
|
265
|
+
const result = await api
|
|
266
|
+
.get('/users')
|
|
267
|
+
.cache({}, 'custom-cache-key')
|
|
268
|
+
.stale(60_000)
|
|
269
|
+
.execute();
|
|
90
270
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
|
|
271
|
+
// Store-based caching (Redis, etc.)
|
|
272
|
+
const api = IgniterCaller.create()
|
|
273
|
+
.withStore(redisAdapter, {
|
|
274
|
+
ttl: 3600,
|
|
275
|
+
keyPrefix: 'api:',
|
|
276
|
+
})
|
|
277
|
+
.build();
|
|
98
278
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const github = useCaller('github')
|
|
105
|
-
const { data, isLoading, error, refetch, invalidate } = github
|
|
106
|
-
.get('/me')
|
|
107
|
-
.useQuery({
|
|
108
|
-
params: {},
|
|
109
|
-
query: {},
|
|
110
|
-
headers: {},
|
|
111
|
-
cookies: {},
|
|
112
|
-
staleTime: 5000,
|
|
113
|
-
enabled: true,
|
|
114
|
-
})
|
|
279
|
+
const result = await api
|
|
280
|
+
.get('/users')
|
|
281
|
+
.stale(300_000) // 5 minutes
|
|
282
|
+
.execute();
|
|
283
|
+
```
|
|
115
284
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
285
|
+
### Fallback Values
|
|
286
|
+
|
|
287
|
+
```typescript
|
|
288
|
+
// Provide fallback if request fails
|
|
289
|
+
const result = await api
|
|
290
|
+
.get('/optional-data')
|
|
291
|
+
.fallback(() => ({ default: 'value' }))
|
|
292
|
+
.execute();
|
|
293
|
+
|
|
294
|
+
// result.data will be fallback value if request fails
|
|
295
|
+
// result.error will still contain the original error
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### axios-Style Direct Requests
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Using .request() method
|
|
302
|
+
const result = await api.request({
|
|
303
|
+
method: 'POST',
|
|
304
|
+
url: '/users',
|
|
305
|
+
body: { name: 'John' },
|
|
306
|
+
headers: { 'X-Custom': 'value' },
|
|
307
|
+
timeout: 5000,
|
|
308
|
+
retry: { maxAttempts: 3, backoff: 'exponential' },
|
|
309
|
+
staleTime: 30_000,
|
|
310
|
+
});
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Interceptors
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
const api = IgniterCaller.create()
|
|
317
|
+
.withBaseUrl('https://api.example.com')
|
|
318
|
+
|
|
319
|
+
// Request interceptor (modify before sending)
|
|
320
|
+
.withRequestInterceptor(async (request) => {
|
|
321
|
+
return {
|
|
322
|
+
...request,
|
|
323
|
+
headers: {
|
|
324
|
+
...request.headers,
|
|
325
|
+
'X-Request-ID': crypto.randomUUID(),
|
|
326
|
+
'X-Timestamp': new Date().toISOString(),
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
// Response interceptor (transform after receiving)
|
|
332
|
+
.withResponseInterceptor(async (response) => {
|
|
333
|
+
// Normalize empty responses
|
|
334
|
+
if (response.data === '') {
|
|
335
|
+
return { ...response, data: null as any };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Add custom metadata
|
|
339
|
+
return {
|
|
340
|
+
...response,
|
|
341
|
+
metadata: {
|
|
342
|
+
cached: response.headers?.get('X-Cache') === 'HIT',
|
|
343
|
+
duration: parseInt(response.headers?.get('X-Duration') || '0'),
|
|
344
|
+
},
|
|
345
|
+
};
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
.build();
|
|
120
349
|
```
|
|
121
350
|
|
|
122
|
-
###
|
|
351
|
+
### Schema Validation (Type-Safe)
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
import { IgniterCaller, IgniterCallerSchema } from '@igniter-js/caller';
|
|
355
|
+
import { z } from 'zod';
|
|
356
|
+
|
|
357
|
+
// Define schemas
|
|
358
|
+
const UserSchema = z.object({
|
|
359
|
+
id: z.string(),
|
|
360
|
+
name: z.string(),
|
|
361
|
+
email: z.string().email(),
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
const ErrorSchema = z.object({
|
|
365
|
+
message: z.string(),
|
|
366
|
+
code: z.string(),
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Build schema registry
|
|
370
|
+
const apiSchemas = IgniterCallerSchema.create()
|
|
371
|
+
.schema('User', UserSchema)
|
|
372
|
+
.schema('Error', ErrorSchema)
|
|
373
|
+
|
|
374
|
+
.path('/users/:id', (path) =>
|
|
375
|
+
path.get({
|
|
376
|
+
responses: {
|
|
377
|
+
200: path.ref('User').schema,
|
|
378
|
+
404: path.ref('Error').schema,
|
|
379
|
+
},
|
|
380
|
+
})
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
.path('/users', (path) =>
|
|
384
|
+
path.get({
|
|
385
|
+
responses: {
|
|
386
|
+
200: path.ref('User').array(),
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
.post({
|
|
390
|
+
request: z.object({
|
|
391
|
+
name: z.string(),
|
|
392
|
+
email: z.string().email(),
|
|
393
|
+
}),
|
|
394
|
+
responses: {
|
|
395
|
+
201: path.ref('User').schema,
|
|
396
|
+
400: path.ref('Error').schema,
|
|
397
|
+
},
|
|
398
|
+
})
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
.build();
|
|
402
|
+
|
|
403
|
+
// Create typed client
|
|
404
|
+
const api = IgniterCaller.create()
|
|
405
|
+
.withBaseUrl('https://api.example.com')
|
|
406
|
+
.withSchemas(apiSchemas, { mode: 'strict' })
|
|
407
|
+
.build();
|
|
408
|
+
|
|
409
|
+
// Full type inference!
|
|
410
|
+
const result = await api.get('/users/:id')
|
|
411
|
+
.params({ id: '123' }) // ✅ params typed from path pattern
|
|
412
|
+
.execute();
|
|
123
413
|
|
|
124
|
-
|
|
125
|
-
|
|
414
|
+
// ✅ result.data is User | undefined (typed from schema)
|
|
415
|
+
console.log(result.data?.name);
|
|
126
416
|
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
417
|
+
// POST with typed body
|
|
418
|
+
const created = await api.post('/users')
|
|
419
|
+
.body({ name: 'John', email: 'john@example.com' }) // ✅ body is typed
|
|
420
|
+
.execute();
|
|
131
421
|
|
|
132
|
-
//
|
|
133
|
-
github.invalidate('/me', { id: 'user_123' })
|
|
422
|
+
// ✅ created.data is User | undefined
|
|
134
423
|
```
|
|
135
424
|
|
|
136
|
-
###
|
|
425
|
+
### Global Events
|
|
137
426
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
- The React client does not automatically wire store/telemetry to avoid bundler contamination.
|
|
427
|
+
```typescript
|
|
428
|
+
import { IgniterCallerManager } from '@igniter-js/caller';
|
|
141
429
|
|
|
142
|
-
|
|
430
|
+
// Listen to all requests
|
|
431
|
+
const unsubscribe = IgniterCallerManager.on(/.*/, (result, ctx) => {
|
|
432
|
+
console.log(`[${ctx.method}] ${ctx.url}`, {
|
|
433
|
+
status: result.status,
|
|
434
|
+
success: !result.error,
|
|
435
|
+
duration: Date.now() - ctx.timestamp,
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
// Listen to specific paths
|
|
440
|
+
IgniterCallerManager.on(/^\/users/, (result, ctx) => {
|
|
441
|
+
if (result.error) {
|
|
442
|
+
console.error('User API failed:', result.error.message);
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
// Listen to exact URL
|
|
447
|
+
IgniterCallerManager.on('/auth/login', (result, ctx) => {
|
|
448
|
+
if (!result.error) {
|
|
449
|
+
console.log('User logged in successfully');
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// Cleanup listener
|
|
454
|
+
unsubscribe();
|
|
455
|
+
```
|
|
143
456
|
|
|
144
|
-
|
|
457
|
+
### Typed Mocking
|
|
145
458
|
|
|
146
|
-
```
|
|
147
|
-
import { IgniterCaller, IgniterCallerMock } from '@igniter-js/caller'
|
|
459
|
+
```typescript
|
|
460
|
+
import { IgniterCaller, IgniterCallerMock } from '@igniter-js/caller';
|
|
461
|
+
import { z } from 'zod';
|
|
148
462
|
|
|
149
463
|
const schemas = {
|
|
150
464
|
'/users/:id': {
|
|
151
465
|
GET: {
|
|
152
466
|
responses: {
|
|
153
|
-
200:
|
|
467
|
+
200: z.object({ id: z.string(), name: z.string() }),
|
|
154
468
|
},
|
|
155
469
|
},
|
|
156
470
|
},
|
|
157
|
-
|
|
471
|
+
'/users': {
|
|
472
|
+
POST: {
|
|
473
|
+
request: z.object({ name: z.string() }),
|
|
474
|
+
responses: {
|
|
475
|
+
201: z.object({ id: z.string(), name: z.string() }),
|
|
476
|
+
},
|
|
477
|
+
},
|
|
478
|
+
},
|
|
479
|
+
};
|
|
158
480
|
|
|
481
|
+
// Create mock
|
|
159
482
|
const mock = IgniterCallerMock.create()
|
|
160
483
|
.withSchemas(schemas)
|
|
484
|
+
|
|
485
|
+
// Static response
|
|
161
486
|
.mock('/users/:id', {
|
|
162
487
|
GET: {
|
|
163
|
-
response: { id: '
|
|
488
|
+
response: { id: 'user_123', name: 'John Doe' },
|
|
489
|
+
status: 200,
|
|
164
490
|
},
|
|
165
491
|
})
|
|
166
|
-
|
|
492
|
+
|
|
493
|
+
// Dynamic response
|
|
494
|
+
.mock('/users', {
|
|
495
|
+
POST: (request) => ({
|
|
496
|
+
response: {
|
|
497
|
+
id: crypto.randomUUID(),
|
|
498
|
+
name: request.body.name,
|
|
499
|
+
},
|
|
500
|
+
status: 201,
|
|
501
|
+
delayMs: 150, // Simulate network delay
|
|
502
|
+
}),
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
.build();
|
|
167
506
|
|
|
507
|
+
// Create API with mock
|
|
168
508
|
const api = IgniterCaller.create()
|
|
169
509
|
.withSchemas(schemas)
|
|
170
510
|
.withMock({ enabled: true, mock })
|
|
171
|
-
.build()
|
|
511
|
+
.build();
|
|
512
|
+
|
|
513
|
+
// All requests use mock
|
|
514
|
+
const user = await api.get('/users/:id').params({ id: '123' }).execute();
|
|
515
|
+
console.log(user.data); // { id: 'user_123', name: 'John Doe' }
|
|
172
516
|
```
|
|
173
517
|
|
|
174
|
-
|
|
518
|
+
---
|
|
175
519
|
|
|
176
|
-
|
|
177
|
-
const mock = IgniterCallerMock.create()
|
|
178
|
-
.withSchemas(schemas)
|
|
179
|
-
.mock('/users/:id', {
|
|
180
|
-
GET: (request) => ({
|
|
181
|
-
response: { id: request.params.id },
|
|
182
|
-
status: 200,
|
|
183
|
-
delayMs: 150,
|
|
184
|
-
}),
|
|
185
|
-
})
|
|
186
|
-
.build()
|
|
187
|
-
```
|
|
520
|
+
## 🌍 Real-World Examples
|
|
188
521
|
|
|
189
|
-
|
|
522
|
+
### Example 1: E-Commerce Product Catalog
|
|
190
523
|
|
|
191
|
-
|
|
192
|
-
|
|
524
|
+
```typescript
|
|
525
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
526
|
+
import { z } from 'zod';
|
|
193
527
|
|
|
194
|
-
|
|
528
|
+
const ProductSchema = z.object({
|
|
529
|
+
id: z.string(),
|
|
530
|
+
name: z.string(),
|
|
531
|
+
price: z.number(),
|
|
532
|
+
inStock: z.boolean(),
|
|
533
|
+
images: z.array(z.string().url()),
|
|
534
|
+
});
|
|
195
535
|
|
|
196
|
-
|
|
536
|
+
const api = IgniterCaller.create()
|
|
537
|
+
.withBaseUrl('https://shop-api.example.com')
|
|
538
|
+
.withHeaders({ 'X-API-Key': process.env.SHOP_API_KEY! })
|
|
539
|
+
.build();
|
|
540
|
+
|
|
541
|
+
// Fetch products with caching
|
|
542
|
+
async function getProducts(category?: string) {
|
|
543
|
+
const result = await api
|
|
544
|
+
.get('/products')
|
|
545
|
+
.params(category ? { category } : {})
|
|
546
|
+
.responseType(z.object({
|
|
547
|
+
products: z.array(ProductSchema),
|
|
548
|
+
total: z.number(),
|
|
549
|
+
}))
|
|
550
|
+
.stale(300_000) // 5 minutes
|
|
551
|
+
.execute();
|
|
552
|
+
|
|
553
|
+
if (result.error) {
|
|
554
|
+
throw new Error(`Failed to fetch products: ${result.error.message}`);
|
|
555
|
+
}
|
|
197
556
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const users = await api.get('/users').execute()
|
|
557
|
+
return result.data;
|
|
558
|
+
}
|
|
201
559
|
|
|
202
|
-
//
|
|
203
|
-
|
|
560
|
+
// Search with debouncing
|
|
561
|
+
let searchAbortController: AbortController | null = null;
|
|
204
562
|
|
|
205
|
-
|
|
206
|
-
|
|
563
|
+
async function searchProducts(query: string) {
|
|
564
|
+
// Cancel previous search
|
|
565
|
+
searchAbortController?.abort();
|
|
566
|
+
searchAbortController = new AbortController();
|
|
207
567
|
|
|
208
|
-
|
|
209
|
-
|
|
568
|
+
const result = await api
|
|
569
|
+
.get('/products/search')
|
|
570
|
+
.params({ q: query })
|
|
571
|
+
.timeout(3000)
|
|
572
|
+
.execute();
|
|
210
573
|
|
|
211
|
-
|
|
212
|
-
|
|
574
|
+
return result.data?.products || [];
|
|
575
|
+
}
|
|
213
576
|
|
|
214
|
-
//
|
|
215
|
-
const
|
|
577
|
+
// Usage
|
|
578
|
+
const products = await getProducts('electronics');
|
|
579
|
+
console.log(`Found ${products.total} products`);
|
|
216
580
|
```
|
|
217
581
|
|
|
218
|
-
|
|
582
|
+
### Example 2: Payment Processing with Retries
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
219
586
|
|
|
220
|
-
|
|
221
|
-
|
|
587
|
+
const api = IgniterCaller.create()
|
|
588
|
+
.withBaseUrl('https://payments-api.example.com')
|
|
589
|
+
.withHeaders({
|
|
590
|
+
'X-API-Key': process.env.PAYMENT_API_KEY!,
|
|
591
|
+
'Content-Type': 'application/json',
|
|
592
|
+
})
|
|
593
|
+
.build();
|
|
594
|
+
|
|
595
|
+
async function processPayment(payment: {
|
|
596
|
+
amount: number;
|
|
597
|
+
currency: string;
|
|
598
|
+
recipient: { accountNumber: string };
|
|
599
|
+
}) {
|
|
600
|
+
const result = await api
|
|
601
|
+
.post('/payments')
|
|
602
|
+
.body(payment)
|
|
603
|
+
.timeout(10_000) // 10 seconds
|
|
604
|
+
.retry(3, {
|
|
605
|
+
baseDelay: 500,
|
|
606
|
+
backoff: 'exponential',
|
|
607
|
+
retryOnStatus: [503, 504], // Only retry on server errors
|
|
608
|
+
})
|
|
609
|
+
.fallback(() => ({
|
|
610
|
+
id: 'fallback',
|
|
611
|
+
status: 'pending',
|
|
612
|
+
message: 'Payment queued for retry',
|
|
613
|
+
}))
|
|
614
|
+
.execute();
|
|
615
|
+
|
|
616
|
+
if (result.error) {
|
|
617
|
+
console.error('Payment failed:', result.error.message);
|
|
618
|
+
// Log to monitoring service
|
|
619
|
+
throw result.error;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return result.data;
|
|
623
|
+
}
|
|
222
624
|
```
|
|
223
625
|
|
|
224
|
-
|
|
626
|
+
### Example 3: Real-Time Analytics Dashboard
|
|
225
627
|
|
|
226
|
-
|
|
628
|
+
```typescript
|
|
629
|
+
import { IgniterCaller, IgniterCallerManager } from '@igniter-js/caller';
|
|
227
630
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
url: '/users',
|
|
232
|
-
body: { name: 'John' },
|
|
233
|
-
headers: { 'X-Custom': 'value' },
|
|
234
|
-
timeout: 5000,
|
|
235
|
-
})
|
|
631
|
+
const api = IgniterCaller.create()
|
|
632
|
+
.withBaseUrl('https://analytics-api.example.com')
|
|
633
|
+
.build();
|
|
236
634
|
|
|
237
|
-
//
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
})
|
|
635
|
+
// Global event listener for monitoring
|
|
636
|
+
IgniterCallerManager.on(/^\/metrics/, (result, ctx) => {
|
|
637
|
+
if (!result.error) {
|
|
638
|
+
console.log(`Metrics fetched in ${Date.now() - ctx.timestamp}ms`);
|
|
639
|
+
}
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
// Polling with cache
|
|
643
|
+
async function startMetricsPolling(intervalMs: number) {
|
|
644
|
+
const poll = async () => {
|
|
645
|
+
const result = await api
|
|
646
|
+
.get('/metrics')
|
|
647
|
+
.params({
|
|
648
|
+
start: new Date(Date.now() - 300_000).toISOString(),
|
|
649
|
+
end: new Date().toISOString(),
|
|
650
|
+
})
|
|
651
|
+
.stale(30_000) // 30 seconds
|
|
652
|
+
.execute();
|
|
653
|
+
|
|
654
|
+
if (!result.error) {
|
|
655
|
+
updateDashboard(result.data);
|
|
656
|
+
}
|
|
657
|
+
};
|
|
243
658
|
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
url: '/health',
|
|
248
|
-
retry: { maxAttempts: 3, backoff: 'exponential' },
|
|
249
|
-
})
|
|
250
|
-
```
|
|
659
|
+
poll(); // Initial fetch
|
|
660
|
+
return setInterval(poll, intervalMs);
|
|
661
|
+
}
|
|
251
662
|
|
|
252
|
-
|
|
663
|
+
const pollInterval = await startMetricsPolling(30_000);
|
|
664
|
+
```
|
|
253
665
|
|
|
254
|
-
|
|
666
|
+
### Example 4: Multi-Tenant SaaS API Client
|
|
255
667
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
| `application/json` | JSON object |
|
|
259
|
-
| `text/xml`, `application/xml` | Text (parse with your XML library) |
|
|
260
|
-
| `text/csv` | Text |
|
|
261
|
-
| `text/html`, `text/plain` | Text |
|
|
262
|
-
| `image/*`, `audio/*`, `video/*` | Blob |
|
|
263
|
-
| `application/pdf`, `application/zip` | Blob |
|
|
264
|
-
| `application/octet-stream` | Blob |
|
|
668
|
+
```typescript
|
|
669
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
265
670
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
671
|
+
function createTenantAPI(tenantId: string, apiKey: string) {
|
|
672
|
+
return IgniterCaller.create()
|
|
673
|
+
.withBaseUrl('https://saas-api.example.com')
|
|
674
|
+
.withHeaders({
|
|
675
|
+
'X-Tenant-ID': tenantId,
|
|
676
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
677
|
+
})
|
|
678
|
+
.build();
|
|
679
|
+
}
|
|
269
680
|
|
|
270
|
-
|
|
271
|
-
const
|
|
681
|
+
const tenant1API = createTenantAPI('tenant_1', process.env.TENANT_1_KEY!);
|
|
682
|
+
const tenant2API = createTenantAPI('tenant_2', process.env.TENANT_2_KEY!);
|
|
272
683
|
|
|
273
|
-
//
|
|
274
|
-
const
|
|
684
|
+
// Isolated requests per tenant
|
|
685
|
+
const tenant1Users = await tenant1API.get('/users').execute();
|
|
686
|
+
const tenant2Users = await tenant2API.get('/users').execute();
|
|
275
687
|
```
|
|
276
688
|
|
|
277
|
-
|
|
689
|
+
### Example 5: GraphQL-Style Batch Requests
|
|
278
690
|
|
|
279
|
-
|
|
691
|
+
```typescript
|
|
692
|
+
import { IgniterCallerManager } from '@igniter-js/caller';
|
|
280
693
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
694
|
+
async function fetchDashboardData() {
|
|
695
|
+
const [users, posts, comments] = await IgniterCallerManager.batch([
|
|
696
|
+
api.get('/users').params({ limit: 10 }).execute(),
|
|
697
|
+
api.get('/posts').params({ limit: 20 }).execute(),
|
|
698
|
+
api.get('/comments').params({ limit: 50 }).execute(),
|
|
699
|
+
]);
|
|
284
700
|
|
|
285
|
-
|
|
701
|
+
return {
|
|
702
|
+
users: users.data,
|
|
703
|
+
posts: posts.data,
|
|
704
|
+
comments: comments.data,
|
|
705
|
+
};
|
|
706
|
+
}
|
|
286
707
|
```
|
|
287
708
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
## 📚 API Reference
|
|
712
|
+
|
|
713
|
+
### IgniterCaller (Main Builder)
|
|
714
|
+
|
|
715
|
+
The main entry point for creating an HTTP client.
|
|
716
|
+
|
|
717
|
+
```typescript
|
|
718
|
+
class IgniterCallerBuilder<TSchemas> {
|
|
719
|
+
static create(): IgniterCallerBuilder<{}>
|
|
720
|
+
|
|
721
|
+
withBaseUrl(url: string): this
|
|
722
|
+
withHeaders(headers: Record<string, string>): this
|
|
723
|
+
withCookies(cookies: Record<string, string>): this
|
|
724
|
+
withLogger(logger: IgniterLogger): this
|
|
725
|
+
withRequestInterceptor(interceptor: RequestInterceptor): this
|
|
726
|
+
withResponseInterceptor(interceptor: ResponseInterceptor): this
|
|
727
|
+
withStore(store: StoreAdapter, options?: StoreOptions): this
|
|
728
|
+
withSchemas<T>(schemas: T, validation?: ValidationOptions): Builder<T>
|
|
729
|
+
withTelemetry(telemetry: TelemetryManager): this
|
|
730
|
+
withMock(config: MockConfig): this
|
|
731
|
+
|
|
732
|
+
build(): IgniterCallerManager<TSchemas>
|
|
733
|
+
}
|
|
734
|
+
```
|
|
291
735
|
|
|
292
|
-
|
|
736
|
+
**Methods:**
|
|
737
|
+
|
|
738
|
+
| Method | Parameters | Returns | Description |
|
|
739
|
+
|--------|------------|---------|-------------|
|
|
740
|
+
| `create()` | None | `Builder<{}>` | Static factory for new builder |
|
|
741
|
+
| `withBaseUrl()` | `url: string` | `this` | Set base URL prefix for all requests |
|
|
742
|
+
| `withHeaders()` | `headers: Record<string, string>` | `this` | Merge default headers into every request |
|
|
743
|
+
| `withCookies()` | `cookies: Record<string, string>` | `this` | Set default cookies (sent as `Cookie` header) |
|
|
744
|
+
| `withLogger()` | `logger: IgniterLogger` | `this` | Attach logger for request lifecycle logging |
|
|
745
|
+
| `withRequestInterceptor()` | `interceptor: Function` | `this` | Add request modifier (runs before fetch) |
|
|
746
|
+
| `withResponseInterceptor()` | `interceptor: Function` | `this` | Add response transformer (runs after fetch) |
|
|
747
|
+
| `withStore()` | `store: Adapter, options?` | `this` | Configure persistent cache (Redis, etc.) |
|
|
748
|
+
| `withSchemas()` | `schemas: Map, validation?` | `Builder<T>` | Enable type inference + validation |
|
|
749
|
+
| `withTelemetry()` | `telemetry: Manager` | `this` | Connect to telemetry system |
|
|
750
|
+
| `withMock()` | `config: MockConfig` | `this` | Enable mock mode for testing |
|
|
751
|
+
| `build()` | None | `Manager` | Build the operational client instance |
|
|
752
|
+
|
|
753
|
+
**Example:**
|
|
754
|
+
|
|
755
|
+
```typescript
|
|
293
756
|
const api = IgniterCaller.create()
|
|
294
757
|
.withBaseUrl('https://api.example.com')
|
|
295
|
-
.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
...request.headers,
|
|
300
|
-
'x-request-id': crypto.randomUUID(),
|
|
301
|
-
},
|
|
302
|
-
}
|
|
303
|
-
})
|
|
304
|
-
.withResponseInterceptor(async (response) => {
|
|
305
|
-
// Example: normalize empty responses
|
|
306
|
-
if (response.data === '') {
|
|
307
|
-
return { ...response, data: null as any }
|
|
308
|
-
}
|
|
309
|
-
return response
|
|
310
|
-
})
|
|
311
|
-
.build()
|
|
758
|
+
.withHeaders({ Authorization: 'Bearer token' })
|
|
759
|
+
.withStore(redisAdapter)
|
|
760
|
+
.withSchemas(schemas)
|
|
761
|
+
.build();
|
|
312
762
|
```
|
|
313
763
|
|
|
314
|
-
|
|
764
|
+
---
|
|
765
|
+
|
|
766
|
+
### IgniterCallerManager (HTTP Client)
|
|
767
|
+
|
|
768
|
+
The operational HTTP client instance for making requests.
|
|
769
|
+
|
|
770
|
+
```typescript
|
|
771
|
+
class IgniterCallerManager<TSchemas> {
|
|
772
|
+
// HTTP Methods
|
|
773
|
+
get<T>(url?: string): RequestBuilder<T>
|
|
774
|
+
post<T>(url?: string): RequestBuilder<T>
|
|
775
|
+
put<T>(url?: string): RequestBuilder<T>
|
|
776
|
+
patch<T>(url?: string): RequestBuilder<T>
|
|
777
|
+
delete<T>(url?: string): RequestBuilder<T>
|
|
778
|
+
head<T>(url?: string): RequestBuilder<T>
|
|
779
|
+
|
|
780
|
+
// Direct execution (axios-style)
|
|
781
|
+
request<T>(options: DirectRequestOptions): Promise<ApiResponse<T>>
|
|
782
|
+
|
|
783
|
+
// Static methods
|
|
784
|
+
static on(pattern: string | RegExp, callback: EventCallback): () => void
|
|
785
|
+
static off(pattern: string | RegExp, callback?: EventCallback): void
|
|
786
|
+
static invalidate(key: string): Promise<void>
|
|
787
|
+
static invalidatePattern(pattern: string): Promise<void>
|
|
788
|
+
static batch<T extends Promise<any>[]>(requests: T): Promise<AwaitedArray<T>>
|
|
789
|
+
}
|
|
790
|
+
```
|
|
315
791
|
|
|
316
|
-
|
|
792
|
+
**Methods:**
|
|
793
|
+
|
|
794
|
+
| Method | Arguments | Returns | Description |
|
|
795
|
+
|--------|-----------|---------|-------------|
|
|
796
|
+
| `get()` | `url?: string` | `RequestBuilder` | Create GET request |
|
|
797
|
+
| `post()` | `url?: string` | `RequestBuilder` | Create POST request |
|
|
798
|
+
| `put()` | `url?: string` | `RequestBuilder` | Create PUT request |
|
|
799
|
+
| `patch()` | `url?: string` | `RequestBuilder` | Create PATCH request |
|
|
800
|
+
| `delete()` | `url?: string` | `RequestBuilder` | Create DELETE request |
|
|
801
|
+
| `head()` | `url?: string` | `RequestBuilder` | Create HEAD request |
|
|
802
|
+
| `request()` | `options: DirectRequestOptions` | `Promise<ApiResponse>` | Execute request directly |
|
|
803
|
+
| `on()` | `pattern, callback` | `unsubscribe: Function` | Register global event listener |
|
|
804
|
+
| `off()` | `pattern, callback?` | `void` | Remove event listener(s) |
|
|
805
|
+
| `invalidate()` | `key: string` | `Promise<void>` | Invalidate specific cache entry |
|
|
806
|
+
| `invalidatePattern()` | `pattern: string` | `Promise<void>` | Invalidate cache by pattern |
|
|
807
|
+
| `batch()` | `requests: Promise[]` | `Promise<Results[]>` | Execute requests in parallel |
|
|
808
|
+
|
|
809
|
+
---
|
|
810
|
+
|
|
811
|
+
### RequestBuilder (Fluent Request API)
|
|
812
|
+
|
|
813
|
+
Per-request configuration builder.
|
|
814
|
+
|
|
815
|
+
```typescript
|
|
816
|
+
class IgniterCallerRequestBuilder<TResponse> {
|
|
817
|
+
url(url: string): this
|
|
818
|
+
body<T>(body: T): this
|
|
819
|
+
params<T>(params: T): this
|
|
820
|
+
headers(headers: Record<string, string>): this
|
|
821
|
+
timeout(ms: number): this
|
|
822
|
+
cache(cache: CacheInit, key?: string): this
|
|
823
|
+
stale(ms: number): this
|
|
824
|
+
retry(attempts: number, options?: RetryOptions): this
|
|
825
|
+
fallback<T>(fn: () => T): this
|
|
826
|
+
responseType<T>(schema?: StandardSchemaV1<T>): RequestBuilder<T>
|
|
827
|
+
|
|
828
|
+
execute(): Promise<ApiResponse<TResponse>>
|
|
829
|
+
}
|
|
830
|
+
```
|
|
317
831
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
832
|
+
**Methods:**
|
|
833
|
+
|
|
834
|
+
| Method | Parameters | Returns | Description |
|
|
835
|
+
|--------|------------|---------|-------------|
|
|
836
|
+
| `url()` | `url: string` | `this` | Set request URL |
|
|
837
|
+
| `body()` | `body: any` | `this` | Set request body (JSON, FormData, Blob) |
|
|
838
|
+
| `params()` | `params: Record<string, any>` | `this` | Set query parameters |
|
|
839
|
+
| `headers()` | `headers: Record<string, string>` | `this` | Merge additional headers |
|
|
840
|
+
| `timeout()` | `ms: number` | `this` | Set request timeout |
|
|
841
|
+
| `cache()` | `cache: CacheInit, key?: string` | `this` | Set cache strategy |
|
|
842
|
+
| `stale()` | `ms: number` | `this` | Set cache stale time |
|
|
843
|
+
| `retry()` | `attempts: number, options?` | `this` | Configure retry behavior |
|
|
844
|
+
| `fallback()` | `fn: () => T` | `this` | Provide fallback value on error |
|
|
845
|
+
| `responseType()` | `schema?: StandardSchemaV1` | `Builder<T>` | Set expected response type |
|
|
846
|
+
| `execute()` | None | `Promise<ApiResponse>` | Execute the request |
|
|
847
|
+
|
|
848
|
+
---
|
|
849
|
+
|
|
850
|
+
### Types
|
|
851
|
+
|
|
852
|
+
#### ApiResponse
|
|
853
|
+
|
|
854
|
+
```typescript
|
|
855
|
+
interface IgniterCallerApiResponse<TData> {
|
|
856
|
+
data?: TData;
|
|
857
|
+
error?: IgniterCallerError;
|
|
858
|
+
status?: number;
|
|
859
|
+
headers?: Headers;
|
|
860
|
+
}
|
|
327
861
|
```
|
|
328
862
|
|
|
329
|
-
|
|
863
|
+
#### RetryOptions
|
|
330
864
|
|
|
331
|
-
|
|
865
|
+
```typescript
|
|
866
|
+
interface IgniterCallerRetryOptions {
|
|
867
|
+
maxAttempts: number;
|
|
868
|
+
baseDelay?: number;
|
|
869
|
+
backoff?: 'linear' | 'exponential';
|
|
870
|
+
retryOnStatus?: number[];
|
|
871
|
+
}
|
|
872
|
+
```
|
|
332
873
|
|
|
333
|
-
|
|
874
|
+
#### ValidationOptions
|
|
334
875
|
|
|
335
|
-
```
|
|
336
|
-
|
|
876
|
+
```typescript
|
|
877
|
+
interface IgniterCallerSchemaValidationOptions {
|
|
878
|
+
mode?: 'strict' | 'soft' | 'off';
|
|
879
|
+
onValidationError?: (error: ValidationError) => void;
|
|
880
|
+
}
|
|
337
881
|
```
|
|
338
882
|
|
|
339
|
-
|
|
883
|
+
---
|
|
884
|
+
|
|
885
|
+
## 🔧 Configuration
|
|
340
886
|
|
|
341
|
-
|
|
887
|
+
### Store Adapter
|
|
342
888
|
|
|
343
|
-
|
|
344
|
-
|
|
889
|
+
Configure persistent caching with Redis or other stores:
|
|
890
|
+
|
|
891
|
+
```typescript
|
|
892
|
+
interface IgniterCallerStoreAdapter<TClient = any> {
|
|
893
|
+
client: TClient | null;
|
|
894
|
+
get(key: string): Promise<string | null>;
|
|
895
|
+
set(key: string, value: string, ttl?: number): Promise<void>;
|
|
896
|
+
delete(key: string): Promise<void>;
|
|
897
|
+
has(key: string): Promise<boolean>;
|
|
898
|
+
}
|
|
345
899
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
async set(key, value) { void key; void value },
|
|
350
|
-
async delete(key) { void key },
|
|
351
|
-
async has(key) { void key; return false },
|
|
900
|
+
interface IgniterCallerStoreOptions {
|
|
901
|
+
ttl?: number;
|
|
902
|
+
keyPrefix?: string;
|
|
352
903
|
}
|
|
904
|
+
```
|
|
905
|
+
|
|
906
|
+
**Example:**
|
|
907
|
+
|
|
908
|
+
```typescript
|
|
909
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
910
|
+
|
|
911
|
+
const redisAdapter: IgniterCallerStoreAdapter = {
|
|
912
|
+
client: redis,
|
|
913
|
+
async get(key) { return await redis.get(key); },
|
|
914
|
+
async set(key, value, ttl) { await redis.setex(key, ttl || 3600, value); },
|
|
915
|
+
async delete(key) { await redis.del(key); },
|
|
916
|
+
async has(key) { return (await redis.exists(key)) === 1; },
|
|
917
|
+
};
|
|
353
918
|
|
|
354
919
|
const api = IgniterCaller.create()
|
|
355
|
-
.withStore(
|
|
920
|
+
.withStore(redisAdapter, {
|
|
356
921
|
ttl: 3600,
|
|
357
|
-
keyPrefix: '
|
|
922
|
+
keyPrefix: 'api:',
|
|
358
923
|
})
|
|
359
|
-
.build()
|
|
924
|
+
.build();
|
|
360
925
|
```
|
|
361
926
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
The package ships a mock store adapter for tests and local development:
|
|
927
|
+
### Schema Validation
|
|
365
928
|
|
|
366
|
-
|
|
367
|
-
import { MockCallerStoreAdapter } from '@igniter-js/caller/adapters'
|
|
368
|
-
import { IgniterCaller } from '@igniter-js/caller'
|
|
929
|
+
Enable runtime validation with any StandardSchemaV1 library:
|
|
369
930
|
|
|
370
|
-
|
|
931
|
+
```typescript
|
|
932
|
+
import { z } from 'zod';
|
|
371
933
|
|
|
372
934
|
const api = IgniterCaller.create()
|
|
373
|
-
.
|
|
374
|
-
|
|
935
|
+
.withSchemas(schemas, {
|
|
936
|
+
mode: 'strict', // 'strict' | 'soft' | 'off'
|
|
937
|
+
onValidationError: (error) => {
|
|
938
|
+
console.error('Validation failed:', error);
|
|
939
|
+
},
|
|
940
|
+
})
|
|
941
|
+
.build();
|
|
375
942
|
```
|
|
376
943
|
|
|
377
|
-
|
|
944
|
+
**Modes:**
|
|
945
|
+
- `strict`: Throw on validation failure (default)
|
|
946
|
+
- `soft`: Log error and continue
|
|
947
|
+
- `off`: Skip validation
|
|
378
948
|
|
|
379
|
-
|
|
380
|
-
Schemas must implement `StandardSchemaV1` (Zod is supported, and any compatible library works).
|
|
949
|
+
---
|
|
381
950
|
|
|
382
|
-
|
|
951
|
+
## 🧪 Testing
|
|
383
952
|
|
|
384
|
-
|
|
385
|
-
import { IgniterCaller, IgniterCallerSchema } from '@igniter-js/caller'
|
|
386
|
-
import { z } from 'zod'
|
|
953
|
+
### Unit Testing with Mock Adapter
|
|
387
954
|
|
|
388
|
-
|
|
389
|
-
|
|
955
|
+
```typescript
|
|
956
|
+
import { describe, it, expect } from 'vitest';
|
|
957
|
+
import { IgniterCaller, IgniterCallerMock } from '@igniter-js/caller';
|
|
958
|
+
import { MockCallerStoreAdapter } from '@igniter-js/caller/adapters';
|
|
390
959
|
|
|
391
|
-
|
|
392
|
-
.
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
960
|
+
describe('API Client', () => {
|
|
961
|
+
const mock = IgniterCallerMock.create()
|
|
962
|
+
.mock('/users/:id', {
|
|
963
|
+
GET: (request) => ({
|
|
964
|
+
response: { id: request.params.id, name: 'Test User' },
|
|
965
|
+
status: 200,
|
|
966
|
+
}),
|
|
967
|
+
})
|
|
968
|
+
.build();
|
|
969
|
+
|
|
970
|
+
const api = IgniterCaller.create()
|
|
971
|
+
.withMock({ enabled: true, mock })
|
|
972
|
+
.build();
|
|
973
|
+
|
|
974
|
+
it('should fetch user', async () => {
|
|
975
|
+
const result = await api.get('/users/:id').params({ id: '123' }).execute();
|
|
976
|
+
|
|
977
|
+
expect(result.error).toBeUndefined();
|
|
978
|
+
expect(result.data).toEqual({ id: '123', name: 'Test User' });
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
it('should handle errors', async () => {
|
|
982
|
+
const mock = IgniterCallerMock.create()
|
|
983
|
+
.mock('/error', {
|
|
984
|
+
GET: { response: null, status: 500 },
|
|
985
|
+
})
|
|
986
|
+
.build();
|
|
987
|
+
|
|
988
|
+
const api = IgniterCaller.create()
|
|
989
|
+
.withMock({ enabled: true, mock })
|
|
990
|
+
.build();
|
|
991
|
+
|
|
992
|
+
const result = await api.get('/error').execute();
|
|
993
|
+
|
|
994
|
+
expect(result.error).toBeDefined();
|
|
995
|
+
});
|
|
996
|
+
});
|
|
997
|
+
```
|
|
406
998
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
999
|
+
### Integration Testing
|
|
1000
|
+
|
|
1001
|
+
```typescript
|
|
1002
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
411
1003
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
1004
|
+
describe('Integration: Real API', () => {
|
|
1005
|
+
const api = IgniterCaller.create()
|
|
1006
|
+
.withBaseUrl(process.env.TEST_API_URL!)
|
|
1007
|
+
.build();
|
|
1008
|
+
|
|
1009
|
+
it('should fetch users from real API', async () => {
|
|
1010
|
+
const result = await api.get('/users').execute();
|
|
1011
|
+
|
|
1012
|
+
expect(result.error).toBeUndefined();
|
|
1013
|
+
expect(Array.isArray(result.data)).toBe(true);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
415
1016
|
```
|
|
416
1017
|
|
|
417
|
-
|
|
418
|
-
`callerSchemas.$Infer` provides type inference without extra imports. `path.ref()` helpers use Zod
|
|
419
|
-
wrappers; when using a different StandardSchema implementation, use `ref().schema` directly.
|
|
1018
|
+
---
|
|
420
1019
|
|
|
421
|
-
|
|
1020
|
+
## 🎨 Best Practices
|
|
422
1021
|
|
|
423
|
-
|
|
424
|
-
import { IgniterCaller } from '@igniter-js/caller'
|
|
425
|
-
import { z } from 'zod'
|
|
1022
|
+
### ✅ Do
|
|
426
1023
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
},
|
|
434
|
-
},
|
|
435
|
-
} as const
|
|
1024
|
+
```typescript
|
|
1025
|
+
// ✅ Use immutable builders
|
|
1026
|
+
const api = IgniterCaller.create()
|
|
1027
|
+
.withBaseUrl('...')
|
|
1028
|
+
.withHeaders({ ... })
|
|
1029
|
+
.build();
|
|
436
1030
|
|
|
1031
|
+
// ✅ Handle errors explicitly
|
|
1032
|
+
const result = await api.get('/users').execute();
|
|
1033
|
+
if (result.error) {
|
|
1034
|
+
console.error(result.error);
|
|
1035
|
+
throw result.error;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
// ✅ Use schema validation for type safety
|
|
437
1039
|
const api = IgniterCaller.create()
|
|
438
|
-
.withBaseUrl('https://api.example.com')
|
|
439
1040
|
.withSchemas(schemas, { mode: 'strict' })
|
|
440
|
-
.build()
|
|
1041
|
+
.build();
|
|
1042
|
+
|
|
1043
|
+
// ✅ Cache expensive requests
|
|
1044
|
+
const result = await api
|
|
1045
|
+
.get('/expensive')
|
|
1046
|
+
.stale(300_000) // 5 minutes
|
|
1047
|
+
.execute();
|
|
441
1048
|
|
|
442
|
-
|
|
1049
|
+
// ✅ Use retry for transient failures
|
|
1050
|
+
const result = await api
|
|
1051
|
+
.get('/unreliable')
|
|
1052
|
+
.retry(3, { backoff: 'exponential' })
|
|
1053
|
+
.execute();
|
|
1054
|
+
|
|
1055
|
+
// ✅ Provide fallbacks for optional data
|
|
1056
|
+
const result = await api
|
|
1057
|
+
.get('/optional')
|
|
1058
|
+
.fallback(() => defaultValue)
|
|
1059
|
+
.execute();
|
|
443
1060
|
```
|
|
444
1061
|
|
|
445
|
-
|
|
1062
|
+
### ❌ Don't
|
|
446
1063
|
|
|
447
|
-
|
|
1064
|
+
```typescript
|
|
1065
|
+
// ❌ Don't mutate builder state
|
|
1066
|
+
const builder = IgniterCaller.create();
|
|
1067
|
+
builder.state.baseURL = 'https://api.example.com'; // ❌ Won't work
|
|
448
1068
|
|
|
449
|
-
|
|
1069
|
+
// ❌ Don't ignore errors
|
|
1070
|
+
const result = await api.get('/users').execute();
|
|
1071
|
+
console.log(result.data); // ❌ Might be undefined
|
|
450
1072
|
|
|
451
|
-
|
|
452
|
-
|
|
1073
|
+
// ❌ Don't skip validation in production
|
|
1074
|
+
const api = IgniterCaller.create()
|
|
1075
|
+
.withSchemas(schemas, { mode: 'off' }) // ❌ Risky
|
|
1076
|
+
.build();
|
|
1077
|
+
|
|
1078
|
+
// ❌ Don't cache mutations
|
|
1079
|
+
const result = await api
|
|
1080
|
+
.post('/users')
|
|
1081
|
+
.stale(60_000) // ❌ Don't cache POST/PUT/PATCH/DELETE
|
|
1082
|
+
.execute();
|
|
1083
|
+
|
|
1084
|
+
// ❌ Don't retry non-idempotent operations
|
|
1085
|
+
const result = await api
|
|
1086
|
+
.post('/payments')
|
|
1087
|
+
.retry(3) // ❌ Might duplicate payment
|
|
1088
|
+
.execute();
|
|
453
1089
|
```
|
|
454
1090
|
|
|
455
|
-
|
|
1091
|
+
---
|
|
1092
|
+
|
|
1093
|
+
## 🚨 Troubleshooting
|
|
456
1094
|
|
|
457
|
-
|
|
458
|
-
import { facebookCaller } from './src/callers/api.example.com'
|
|
459
|
-
import { facebookCallerSchemas } from './src/callers/api.example.com/schema'
|
|
1095
|
+
### Error: Request timeout
|
|
460
1096
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
1097
|
+
**Cause:** Request took longer than configured timeout
|
|
1098
|
+
|
|
1099
|
+
**Solution:**
|
|
1100
|
+
|
|
1101
|
+
```typescript
|
|
1102
|
+
// Increase timeout
|
|
1103
|
+
const result = await api
|
|
1104
|
+
.get('/slow-endpoint')
|
|
1105
|
+
.timeout(30_000) // 30 seconds
|
|
1106
|
+
.execute();
|
|
465
1107
|
```
|
|
466
1108
|
|
|
467
|
-
|
|
468
|
-
schemas, and includes derived type aliases for each endpoint.
|
|
1109
|
+
---
|
|
469
1110
|
|
|
470
|
-
|
|
1111
|
+
### Error: Validation failed
|
|
471
1112
|
|
|
472
|
-
|
|
1113
|
+
**Cause:** Response doesn't match schema
|
|
473
1114
|
|
|
474
|
-
|
|
475
|
-
2. **Validate the response** - if you pass a Zod/StandardSchema (only for JSON/XML/CSV)
|
|
1115
|
+
**Solution:**
|
|
476
1116
|
|
|
477
|
-
```
|
|
478
|
-
|
|
1117
|
+
```typescript
|
|
1118
|
+
// Check schema definition
|
|
1119
|
+
const UserSchema = z.object({
|
|
1120
|
+
id: z.string(),
|
|
1121
|
+
name: z.string(), // ❌ API returns `username`
|
|
1122
|
+
});
|
|
479
1123
|
|
|
480
|
-
//
|
|
481
|
-
const
|
|
482
|
-
.
|
|
483
|
-
|
|
484
|
-
|
|
1124
|
+
// Fix schema
|
|
1125
|
+
const UserSchema = z.object({
|
|
1126
|
+
id: z.string(),
|
|
1127
|
+
username: z.string(), // ✅ Matches API
|
|
1128
|
+
});
|
|
485
1129
|
|
|
486
|
-
//
|
|
487
|
-
const
|
|
1130
|
+
// Or use soft mode
|
|
1131
|
+
const api = IgniterCaller.create()
|
|
1132
|
+
.withSchemas(schemas, { mode: 'soft' })
|
|
1133
|
+
.build();
|
|
488
1134
|
```
|
|
489
1135
|
|
|
490
|
-
|
|
1136
|
+
---
|
|
491
1137
|
|
|
492
|
-
|
|
1138
|
+
### Error: Cache not invalidating
|
|
493
1139
|
|
|
494
|
-
|
|
495
|
-
import { IgniterCallerManager } from '@igniter-js/caller'
|
|
1140
|
+
**Cause:** Cache key doesn't match
|
|
496
1141
|
|
|
497
|
-
|
|
498
|
-
console.log(`[${ctx.method}] ${ctx.url}`, {
|
|
499
|
-
ok: !result.error,
|
|
500
|
-
status: result.status,
|
|
501
|
-
})
|
|
502
|
-
})
|
|
1142
|
+
**Solution:**
|
|
503
1143
|
|
|
504
|
-
|
|
505
|
-
|
|
1144
|
+
```typescript
|
|
1145
|
+
// Ensure consistent cache keys
|
|
1146
|
+
const result1 = await api.get('/users').cache({}, 'users-list').execute();
|
|
1147
|
+
|
|
1148
|
+
// Later, invalidate with same key
|
|
1149
|
+
await IgniterCallerManager.invalidate('users-list');
|
|
506
1150
|
```
|
|
507
1151
|
|
|
508
|
-
|
|
1152
|
+
---
|
|
1153
|
+
|
|
1154
|
+
### Performance: Slow requests
|
|
509
1155
|
|
|
510
|
-
|
|
511
|
-
import { IgniterTelemetry } from '@igniter-js/telemetry'
|
|
512
|
-
import { IgniterCaller } from '@igniter-js/caller'
|
|
513
|
-
import { IgniterCallerTelemetryEvents } from '@igniter-js/caller/telemetry'
|
|
1156
|
+
**Diagnosis:** No caching or retries
|
|
514
1157
|
|
|
515
|
-
|
|
516
|
-
.withService('my-api')
|
|
517
|
-
.addEvents(IgniterCallerTelemetryEvents)
|
|
518
|
-
.build()
|
|
1158
|
+
**Solution:**
|
|
519
1159
|
|
|
1160
|
+
```typescript
|
|
1161
|
+
// Enable caching for read-heavy endpoints
|
|
1162
|
+
const result = await api
|
|
1163
|
+
.get('/heavy-computation')
|
|
1164
|
+
.stale(600_000) // 10 minutes
|
|
1165
|
+
.execute();
|
|
1166
|
+
|
|
1167
|
+
// Use store-based cache for persistence
|
|
520
1168
|
const api = IgniterCaller.create()
|
|
521
|
-
.
|
|
522
|
-
.
|
|
523
|
-
.build()
|
|
1169
|
+
.withStore(redisAdapter)
|
|
1170
|
+
.build();
|
|
524
1171
|
```
|
|
525
1172
|
|
|
526
|
-
|
|
1173
|
+
---
|
|
527
1174
|
|
|
528
|
-
|
|
1175
|
+
### Type Inference: Not working
|
|
529
1176
|
|
|
530
|
-
|
|
531
|
-
import { IgniterCallerError } from '@igniter-js/caller'
|
|
1177
|
+
**Cause:** Schema path doesn't match request URL
|
|
532
1178
|
|
|
533
|
-
|
|
1179
|
+
**Solution:**
|
|
534
1180
|
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1181
|
+
```typescript
|
|
1182
|
+
// ❌ Schema path doesn't match
|
|
1183
|
+
const schemas = {
|
|
1184
|
+
'/users': { GET: { responses: { 200: UserSchema } } }
|
|
1185
|
+
};
|
|
1186
|
+
|
|
1187
|
+
const result = await api.get('/users/list').execute(); // ❌ No match
|
|
1188
|
+
|
|
1189
|
+
// ✅ Fix schema or URL
|
|
1190
|
+
const schemas = {
|
|
1191
|
+
'/users/list': { GET: { responses: { 200: UserSchema } } }
|
|
1192
|
+
};
|
|
1193
|
+
|
|
1194
|
+
const result = await api.get('/users/list').execute(); // ✅ Typed
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
---
|
|
1198
|
+
|
|
1199
|
+
## 🔗 Framework Integration
|
|
1200
|
+
|
|
1201
|
+
### Next.js (App Router)
|
|
1202
|
+
|
|
1203
|
+
```typescript
|
|
1204
|
+
// lib/api.ts
|
|
1205
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
1206
|
+
|
|
1207
|
+
export const api = IgniterCaller.create()
|
|
1208
|
+
.withBaseUrl(process.env.NEXT_PUBLIC_API_URL!)
|
|
1209
|
+
.build();
|
|
1210
|
+
|
|
1211
|
+
// app/users/page.tsx
|
|
1212
|
+
import { api } from '@/lib/api';
|
|
1213
|
+
|
|
1214
|
+
export default async function UsersPage() {
|
|
1215
|
+
const result = await api.get('/users').execute();
|
|
1216
|
+
|
|
1217
|
+
if (result.error) {
|
|
1218
|
+
throw new Error('Failed to fetch users');
|
|
538
1219
|
}
|
|
539
|
-
|
|
1220
|
+
|
|
1221
|
+
return (
|
|
1222
|
+
<div>
|
|
1223
|
+
{result.data.map((user) => (
|
|
1224
|
+
<div key={user.id}>{user.name}</div>
|
|
1225
|
+
))}
|
|
1226
|
+
</div>
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
### React with TanStack Query
|
|
1232
|
+
|
|
1233
|
+
```typescript
|
|
1234
|
+
import { useQuery } from '@tanstack/react-query';
|
|
1235
|
+
import { api } from './api';
|
|
1236
|
+
|
|
1237
|
+
function useUsers() {
|
|
1238
|
+
return useQuery({
|
|
1239
|
+
queryKey: ['users'],
|
|
1240
|
+
queryFn: async () => {
|
|
1241
|
+
const result = await api.get('/users').execute();
|
|
1242
|
+
if (result.error) throw result.error;
|
|
1243
|
+
return result.data;
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
540
1246
|
}
|
|
541
1247
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1248
|
+
function Users() {
|
|
1249
|
+
const { data, isLoading, error } = useUsers();
|
|
1250
|
+
|
|
1251
|
+
if (isLoading) return <div>Loading...</div>;
|
|
1252
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1253
|
+
|
|
1254
|
+
return <ul>{data.map(...)}</ul>;
|
|
1255
|
+
}
|
|
545
1256
|
```
|
|
546
1257
|
|
|
547
|
-
|
|
1258
|
+
### Express.js
|
|
548
1259
|
|
|
549
|
-
|
|
1260
|
+
```typescript
|
|
1261
|
+
import express from 'express';
|
|
1262
|
+
import { IgniterCaller } from '@igniter-js/caller';
|
|
550
1263
|
|
|
551
|
-
|
|
1264
|
+
const app = express();
|
|
1265
|
+
const api = IgniterCaller.create()
|
|
1266
|
+
.withBaseUrl('https://external-api.example.com')
|
|
1267
|
+
.build();
|
|
1268
|
+
|
|
1269
|
+
app.get('/proxy/users', async (req, res) => {
|
|
1270
|
+
const result = await api.get('/users').execute();
|
|
1271
|
+
|
|
1272
|
+
if (result.error) {
|
|
1273
|
+
return res.status(result.status || 500).json({
|
|
1274
|
+
error: result.error.message,
|
|
1275
|
+
});
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
res.json(result.data);
|
|
1279
|
+
});
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
---
|
|
1283
|
+
|
|
1284
|
+
## 📊 Performance Tips
|
|
1285
|
+
|
|
1286
|
+
1. **Use caching aggressively** for read-heavy endpoints
|
|
1287
|
+
2. **Enable store-based caching** (Redis) for distributed systems
|
|
1288
|
+
3. **Batch parallel requests** with `IgniterCallerManager.batch()`
|
|
1289
|
+
4. **Set appropriate timeouts** to fail fast
|
|
1290
|
+
5. **Use retry with exponential backoff** for transient failures
|
|
1291
|
+
6. **Minimize interceptor overhead** (avoid heavy computation)
|
|
1292
|
+
7. **Enable compression** via headers (`Accept-Encoding: gzip`)
|
|
552
1293
|
|
|
553
|
-
|
|
1294
|
+
---
|
|
554
1295
|
|
|
555
|
-
|
|
556
|
-
|--------|-------------|
|
|
557
|
-
| `.withBaseUrl(url)` | Sets the base URL for all requests |
|
|
558
|
-
| `.withHeaders(headers)` | Sets default headers |
|
|
559
|
-
| `.withCookies(cookies)` | Sets default cookies |
|
|
560
|
-
| `.withLogger(logger)` | Attaches a logger |
|
|
561
|
-
| `.withRequestInterceptor(fn)` | Adds a request interceptor |
|
|
562
|
-
| `.withResponseInterceptor(fn)` | Adds a response interceptor |
|
|
563
|
-
| `.withStore(store, options)` | Configures a persistent store |
|
|
564
|
-
| `.withSchemas(schemas, options)` | Configures schema validation |
|
|
565
|
-
| `.withTelemetry(telemetry)` | Attaches telemetry manager |
|
|
566
|
-
| `.build()` | Builds the caller instance |
|
|
1296
|
+
## 🤝 Contributing
|
|
567
1297
|
|
|
568
|
-
|
|
1298
|
+
Contributions are welcome! See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines.
|
|
1299
|
+
|
|
1300
|
+
### Development Setup
|
|
1301
|
+
|
|
1302
|
+
```bash
|
|
1303
|
+
git clone https://github.com/felipebarcelospro/igniter-js.git
|
|
1304
|
+
cd igniter-js/packages/caller
|
|
1305
|
+
npm install
|
|
1306
|
+
npm run build
|
|
1307
|
+
npm test
|
|
1308
|
+
```
|
|
569
1309
|
|
|
570
|
-
|
|
571
|
-
|--------|-------------|
|
|
572
|
-
| `.get(url?)` | Creates a GET request |
|
|
573
|
-
| `.post(url?)` | Creates a POST request |
|
|
574
|
-
| `.put(url?)` | Creates a PUT request |
|
|
575
|
-
| `.patch(url?)` | Creates a PATCH request |
|
|
576
|
-
| `.delete(url?)` | Creates a DELETE request |
|
|
577
|
-
| `.head(url?)` | Creates a HEAD request |
|
|
578
|
-
| `.request(options)` | Executes request directly (axios-style) |
|
|
1310
|
+
---
|
|
579
1311
|
|
|
580
|
-
|
|
1312
|
+
## 📄 License
|
|
581
1313
|
|
|
582
|
-
|
|
583
|
-
|--------|-------------|
|
|
584
|
-
| `.url(url)` | Sets the URL |
|
|
585
|
-
| `.body(body)` | Sets the request body |
|
|
586
|
-
| `.params(params)` | Sets query parameters |
|
|
587
|
-
| `.headers(headers)` | Merges additional headers |
|
|
588
|
-
| `.timeout(ms)` | Sets request timeout |
|
|
589
|
-
| `.cache(cache, key?)` | Sets cache strategy |
|
|
590
|
-
| `.stale(ms)` | Sets cache stale time |
|
|
591
|
-
| `.retry(attempts, options)` | Configures retry behavior |
|
|
592
|
-
| `.fallback(fn)` | Provides fallback value |
|
|
593
|
-
| `.responseType(schema?)` | Sets expected response type |
|
|
594
|
-
| `.execute()` | Executes the request |
|
|
1314
|
+
MIT © [Felipe Barcelos](https://github.com/felipebarcelospro)
|
|
595
1315
|
|
|
596
|
-
|
|
1316
|
+
---
|
|
597
1317
|
|
|
598
|
-
|
|
599
|
-
|--------|-------------|
|
|
600
|
-
| `IgniterCallerManager.on(pattern, callback)` | Registers event listener |
|
|
601
|
-
| `IgniterCallerManager.off(pattern, callback?)` | Removes event listener |
|
|
602
|
-
| `IgniterCallerManager.invalidate(key)` | Invalidates cache entry |
|
|
603
|
-
| `IgniterCallerManager.invalidatePattern(pattern)` | Invalidates cache by pattern |
|
|
604
|
-
| `IgniterCallerManager.batch(requests)` | Executes requests in parallel |
|
|
1318
|
+
## 🔗 Related Packages
|
|
605
1319
|
|
|
606
|
-
|
|
1320
|
+
- [@igniter-js/core](../core) — HTTP framework core
|
|
1321
|
+
- [@igniter-js/telemetry](../telemetry) — Observability system
|
|
1322
|
+
- [@igniter-js/store](../store) — State management
|
|
1323
|
+
- [Igniter.js Documentation](https://igniterjs.com)
|
|
607
1324
|
|
|
608
|
-
|
|
1325
|
+
---
|
|
609
1326
|
|
|
610
|
-
##
|
|
1327
|
+
## 💬 Community & Support
|
|
611
1328
|
|
|
612
|
-
|
|
1329
|
+
- 📚 [Documentation](https://igniterjs.com/docs/caller)
|
|
1330
|
+
- 💬 [Discord Community](https://discord.gg/igniterjs)
|
|
1331
|
+
- 🐛 [Report Issues](https://github.com/felipebarcelospro/igniter-js/issues)
|
|
1332
|
+
- 🔒 [Security Policy](https://github.com/felipebarcelospro/igniter-js/security/policy)
|
|
613
1333
|
|
|
614
|
-
|
|
1334
|
+
---
|
|
615
1335
|
|
|
616
|
-
|
|
617
|
-
- **GitHub:** https://github.com/felipebarcelospro/igniter-js
|
|
618
|
-
- **NPM:** https://www.npmjs.com/package/@igniter-js/caller
|
|
619
|
-
- **Issues:** https://github.com/felipebarcelospro/igniter-js/issues
|
|
1336
|
+
**Built with ❤️ by the Igniter.js team**
|