@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 CHANGED
@@ -1,619 +1,1336 @@
1
1
  # @igniter-js/caller
2
2
 
3
- [![NPM Version](https://img.shields.io/npm/v/@igniter-js/caller.svg)](https://www.npmjs.com/package/@igniter-js/caller)
4
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
3
+ <div align="center">
5
4
 
6
- Type-safe HTTP client for Igniter.js apps. Built on top of `fetch`, it gives you a fluent request builder, interceptors, retries, caching (memory or store), schema validation (Standard Schema V1), and global response events.
5
+ [![npm version](https://img.shields.io/npm/v/@igniter-js/caller)](https://www.npmjs.com/package/@igniter-js/caller)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.6+-blue)](https://www.typescriptlang.org/)
8
+ [![Node.js](https://img.shields.io/badge/Node.js-18+-green)](https://nodejs.org/)
9
+ [![Bun](https://img.shields.io/badge/Bun-1.0+-orange)](https://bun.sh)
7
10
 
8
- ## Features
11
+ **End-to-end type-safe HTTP client**
12
+ Built on `fetch` with interceptors, retries, caching, schema validation, and full observability.
9
13
 
10
- - **Fluent API** - `api.get('/users').execute()` or builder pattern
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
- ## Installation
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 @igniter-js/common
38
+ # Using npm
39
+ npm install @igniter-js/caller
27
40
 
28
- # pnpm
29
- pnpm add @igniter-js/caller @igniter-js/common
41
+ # Using pnpm
42
+ pnpm add @igniter-js/caller
30
43
 
31
- # yarn
32
- yarn add @igniter-js/caller @igniter-js/common
44
+ # Using yarn
45
+ yarn add @igniter-js/caller
33
46
 
34
- # bun
35
- bun add @igniter-js/caller @igniter-js/common
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
- # Telemetry (optional)
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
- # Schema validation (optional - Zod or any StandardSchemaV1-compatible lib)
45
- npm install zod
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
- > `@igniter-js/common` is required. `@igniter-js/telemetry` and `zod` are optional peer dependencies.
88
+ **✅ Success!** You just made a type-safe HTTP request with zero configuration.
49
89
 
50
- ## Quick Start
90
+ ---
51
91
 
52
- ```ts
53
- import { IgniterCaller } from '@igniter-js/caller'
92
+ ## 🎯 Core Concepts
54
93
 
55
- export const api = IgniterCaller.create()
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
- // Simple GET request with URL directly
61
- const result = await api.get('/users').execute()
199
+ console.log('Users:', users.data);
200
+ ```
62
201
 
63
- // With query params
64
- const result = await api.get('/users').params({ page: 1 }).execute()
202
+ ### Query Parameters
65
203
 
66
- // With caching
67
- const result = await api.get('/users').stale(10_000).execute()
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
- if (result.error) {
70
- throw result.error
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
- console.log(result.data)
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
- ## React Client (Provider + Hooks)
235
+ ### Timeout & Retry
77
236
 
78
- The React client is exposed via the dedicated subpath export:
237
+ ```typescript
238
+ // Set timeout
239
+ const result = await api
240
+ .get('/slow-endpoint')
241
+ .timeout(5000) // 5 seconds
242
+ .execute();
79
243
 
80
- ```ts
81
- import { IgniterCallerProvider, useIgniterCaller } from '@igniter-js/caller/client'
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
- This keeps the root entrypoint server-safe and avoids bundling React in non-React environments.
255
+ ### Caching
85
256
 
86
- ### Basic usage
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
- ```tsx
89
- import { IgniterCallerProvider, useIgniterCaller } from '@igniter-js/caller/client'
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
- export function CallerProvider() {
92
- return (
93
- <IgniterCallerProvider callers={{ github: githubApi }}>
94
- <UserProfile />
95
- </IgniterCallerProvider>
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
- export const useCaller = useIgniterCaller<{
100
- github: typeof githubApi
101
- }>()
102
-
103
- function UserProfile() {
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
- if (isLoading) return <div>Loading...</div>
117
- if (error) return <div>Error</div>
118
- return <pre>{JSON.stringify(data, null, 2)}</pre>
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
- ### Global config and invalidation
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
- ```tsx
125
- const github = useCaller('github')
414
+ // ✅ result.data is User | undefined (typed from schema)
415
+ console.log(result.data?.name);
126
416
 
127
- // Global defaults
128
- github.config.set('headers', { Authorization: 'Bearer ...' })
129
- github.config.set('cookies', { session: '...' })
130
- github.config.set('query', { locale: 'en' })
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
- // Typed invalidation with optional optimistic data
133
- github.invalidate('/me', { id: 'user_123' })
422
+ // created.data is User | undefined
134
423
  ```
135
424
 
136
- ### Client safety notes
425
+ ### Global Events
137
426
 
138
- - If your Caller instance uses store adapters or telemetry managers that are server-only,
139
- do not bundle that instance into browser code. Create a browser-safe instance instead.
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
- ## Mocking (IgniterCallerMock)
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
- Use `IgniterCallerMock` to create a type-safe mock registry based on your schemas.
457
+ ### Typed Mocking
145
458
 
146
- ```ts
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: UserSchema,
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: 'user_1' },
488
+ response: { id: 'user_123', name: 'John Doe' },
489
+ status: 200,
164
490
  },
165
491
  })
166
- .build()
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
- Mock handlers receive the full request context:
518
+ ---
175
519
 
176
- ```ts
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
- Notes:
522
+ ### Example 1: E-Commerce Product Catalog
190
523
 
191
- - If mock is enabled but there is no handler for a request, it falls back to the real request.
192
- - Mock responses can include `status`, `headers`, and optional `delayMs`.
524
+ ```typescript
525
+ import { IgniterCaller } from '@igniter-js/caller';
526
+ import { z } from 'zod';
193
527
 
194
- ## HTTP Methods
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
- All HTTP methods accept an optional URL directly:
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
- ```ts
199
- // GET
200
- const users = await api.get('/users').execute()
557
+ return result.data;
558
+ }
201
559
 
202
- // POST with body
203
- const created = await api.post('/users').body({ name: 'John' }).execute()
560
+ // Search with debouncing
561
+ let searchAbortController: AbortController | null = null;
204
562
 
205
- // PUT
206
- const updated = await api.put('/users/1').body({ name: 'Jane' }).execute()
563
+ async function searchProducts(query: string) {
564
+ // Cancel previous search
565
+ searchAbortController?.abort();
566
+ searchAbortController = new AbortController();
207
567
 
208
- // PATCH
209
- const patched = await api.patch('/users/1').body({ name: 'Jane' }).execute()
568
+ const result = await api
569
+ .get('/products/search')
570
+ .params({ q: query })
571
+ .timeout(3000)
572
+ .execute();
210
573
 
211
- // DELETE
212
- const deleted = await api.delete('/users/1').execute()
574
+ return result.data?.products || [];
575
+ }
213
576
 
214
- // HEAD
215
- const head = await api.head('/users').execute()
577
+ // Usage
578
+ const products = await getProducts('electronics');
579
+ console.log(`Found ${products.total} products`);
216
580
  ```
217
581
 
218
- You can also use the traditional builder pattern:
582
+ ### Example 2: Payment Processing with Retries
583
+
584
+ ```typescript
585
+ import { IgniterCaller } from '@igniter-js/caller';
219
586
 
220
- ```ts
221
- const result = await api.get().url('/users').params({ page: 1 }).execute()
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
- ## axios-style Requests
626
+ ### Example 3: Real-Time Analytics Dashboard
225
627
 
226
- For dynamic requests or when you prefer an object-based API:
628
+ ```typescript
629
+ import { IgniterCaller, IgniterCallerManager } from '@igniter-js/caller';
227
630
 
228
- ```ts
229
- const result = await api.request({
230
- method: 'POST',
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
- // With caching
238
- const result = await api.request({
239
- method: 'GET',
240
- url: '/users',
241
- staleTime: 30000,
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
- // With retry
245
- const result = await api.request({
246
- method: 'GET',
247
- url: '/health',
248
- retry: { maxAttempts: 3, backoff: 'exponential' },
249
- })
250
- ```
659
+ poll(); // Initial fetch
660
+ return setInterval(poll, intervalMs);
661
+ }
251
662
 
252
- ## Auto Content-Type Detection
663
+ const pollInterval = await startMetricsPolling(30_000);
664
+ ```
253
665
 
254
- The response is automatically parsed based on the `Content-Type` header:
666
+ ### Example 4: Multi-Tenant SaaS API Client
255
667
 
256
- | Content-Type | Parsed As |
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
- ```ts
267
- // JSON response - automatically parsed
268
- const { data } = await api.get('/users').execute()
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
- // Blob response - automatically detected
271
- const { data } = await api.get('/file.pdf').responseType<Blob>().execute()
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
- // Stream response
274
- const { data } = await api.get('/stream').responseType<ReadableStream>().execute()
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
- ## GET with Body Query Params
689
+ ### Example 5: GraphQL-Style Batch Requests
278
690
 
279
- When you pass a body to a GET request, it's automatically converted to query parameters:
691
+ ```typescript
692
+ import { IgniterCallerManager } from '@igniter-js/caller';
280
693
 
281
- ```ts
282
- // This:
283
- await api.get('/search').body({ q: 'test', page: 1 }).execute()
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
- // Becomes: GET /search?q=test&page=1
701
+ return {
702
+ users: users.data,
703
+ posts: posts.data,
704
+ comments: comments.data,
705
+ };
706
+ }
286
707
  ```
287
708
 
288
- ## Interceptors
289
-
290
- Interceptors are great for cross-cutting concerns like auth headers, request ids, logging, and response normalization.
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
- ```ts
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
- .withRequestInterceptor(async (request) => {
296
- return {
297
- ...request,
298
- headers: {
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
- ## Retries
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
- Configure retry behavior for transient errors:
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
- ```ts
319
- const result = await api
320
- .get('/health')
321
- .retry(3, {
322
- baseDelay: 250,
323
- backoff: 'exponential',
324
- retryOnStatus: [408, 429, 500, 502, 503, 504],
325
- })
326
- .execute()
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
- ## Caching
863
+ #### RetryOptions
330
864
 
331
- ### In-memory caching
865
+ ```typescript
866
+ interface IgniterCallerRetryOptions {
867
+ maxAttempts: number;
868
+ baseDelay?: number;
869
+ backoff?: 'linear' | 'exponential';
870
+ retryOnStatus?: number[];
871
+ }
872
+ ```
332
873
 
333
- Use `.stale(ms)` to enable caching. The cache key defaults to the request URL, or you can set it via `.cache(cache, key)`.
874
+ #### ValidationOptions
334
875
 
335
- ```ts
336
- const users = await api.get('/users').stale(30_000).execute()
876
+ ```typescript
877
+ interface IgniterCallerSchemaValidationOptions {
878
+ mode?: 'strict' | 'soft' | 'off';
879
+ onValidationError?: (error: ValidationError) => void;
880
+ }
337
881
  ```
338
882
 
339
- ### Store-based caching
883
+ ---
884
+
885
+ ## 🔧 Configuration
340
886
 
341
- You can plug any store that matches `IgniterCallerStoreAdapter` (Redis, etc.).
887
+ ### Store Adapter
342
888
 
343
- ```ts
344
- import { IgniterCaller } from '@igniter-js/caller'
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
- const store = {
347
- client: null,
348
- async get(key) { return null },
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(store, {
920
+ .withStore(redisAdapter, {
356
921
  ttl: 3600,
357
- keyPrefix: 'igniter:caller:',
922
+ keyPrefix: 'api:',
358
923
  })
359
- .build()
924
+ .build();
360
925
  ```
361
926
 
362
- ## Adapters
363
-
364
- The package ships a mock store adapter for tests and local development:
927
+ ### Schema Validation
365
928
 
366
- ```ts
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
- const store = MockCallerStoreAdapter.create()
931
+ ```typescript
932
+ import { z } from 'zod';
371
933
 
372
934
  const api = IgniterCaller.create()
373
- .withStore(store)
374
- .build()
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
- ## Schema Validation (StandardSchemaV1)
944
+ **Modes:**
945
+ - `strict`: Throw on validation failure (default)
946
+ - `soft`: Log error and continue
947
+ - `off`: Skip validation
378
948
 
379
- If you already use schemas in your Igniter.js app, you can validate requests and responses automatically.
380
- Schemas must implement `StandardSchemaV1` (Zod is supported, and any compatible library works).
949
+ ---
381
950
 
382
- ### Preferred: IgniterCallerSchema builder
951
+ ## 🧪 Testing
383
952
 
384
- ```ts
385
- import { IgniterCaller, IgniterCallerSchema } from '@igniter-js/caller'
386
- import { z } from 'zod'
953
+ ### Unit Testing with Mock Adapter
387
954
 
388
- const UserSchema = z.object({ id: z.string(), name: z.string() })
389
- const ErrorSchema = z.object({ message: z.string() })
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
- const callerSchemas = IgniterCallerSchema.create()
392
- .schema('User', UserSchema)
393
- .schema('Error', ErrorSchema)
394
- .path('/users/:id', (path) =>
395
- path.get({
396
- responses: {
397
- 200: path.ref('User').schema,
398
- 404: path.ref('Error').schema,
399
- },
400
- doc: 'Get user by id',
401
- tags: ['users'],
402
- operationId: 'users.get',
403
- }),
404
- )
405
- .build()
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
- const api = IgniterCaller.create()
408
- .withBaseUrl('https://api.example.com')
409
- .withSchemas(callerSchemas, { mode: 'strict' })
410
- .build()
999
+ ### Integration Testing
1000
+
1001
+ ```typescript
1002
+ import { IgniterCaller } from '@igniter-js/caller';
411
1003
 
412
- type UserResponse = ReturnType<
413
- typeof callerSchemas.$Infer.Response<'/users/:id', 'GET', 200>
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
- `callerSchemas.get` exposes runtime helpers (`path`, `endpoint`, `request`, `response`, `schema`) and
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
- ### Manual object literal (still supported)
1020
+ ## 🎨 Best Practices
422
1021
 
423
- ```ts
424
- import { IgniterCaller } from '@igniter-js/caller'
425
- import { z } from 'zod'
1022
+ ### ✅ Do
426
1023
 
427
- const schemas = {
428
- '/users/:id': {
429
- GET: {
430
- responses: {
431
- 200: z.object({ id: z.string(), name: z.string() }),
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
- const result = await api.get('/users/123').execute()
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
- **Note:** Schema validation only runs for validatable content types (JSON, XML, CSV). Binary responses (Blob, Stream) are not validated.
1062
+ ### Don't
446
1063
 
447
- ## Generate schemas via CLI
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
- You can bootstrap Zod schemas and a ready-to-use caller from an OpenAPI 3 spec using the Igniter CLI:
1069
+ // Don't ignore errors
1070
+ const result = await api.get('/users').execute();
1071
+ console.log(result.data); // ❌ Might be undefined
450
1072
 
451
- ```bash
452
- npx @igniter-js/cli generate caller --name facebook --url https://api.example.com/openapi.json
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
- By default this outputs `src/callers/<hostname>/schema.ts` and `index.ts`:
1091
+ ---
1092
+
1093
+ ## 🚨 Troubleshooting
456
1094
 
457
- ```ts
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
- const result = await facebookCaller.get('/products').execute()
462
- type ProductsResponse = ReturnType<
463
- typeof facebookCallerSchemas.$Infer.Response<'/products', 'GET', 200>
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
- The generated `schema.ts` uses `IgniterCallerSchema` (path-first builder), registers reusable
468
- schemas, and includes derived type aliases for each endpoint.
1109
+ ---
469
1110
 
470
- ## `responseType()` for Typing and Validation
1111
+ ### Error: Validation failed
471
1112
 
472
- Use `responseType()` to:
1113
+ **Cause:** Response doesn't match schema
473
1114
 
474
- 1. **Type the response** - for TypeScript inference
475
- 2. **Validate the response** - if you pass a Zod/StandardSchema (only for JSON/XML/CSV)
1115
+ **Solution:**
476
1116
 
477
- ```ts
478
- import { z } from 'zod'
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
- // With Zod schema - validates JSON response
481
- const result = await api
482
- .get('/users')
483
- .responseType(z.array(z.object({ id: z.string(), name: z.string() })))
484
- .execute()
1124
+ // Fix schema
1125
+ const UserSchema = z.object({
1126
+ id: z.string(),
1127
+ username: z.string(), // Matches API
1128
+ });
485
1129
 
486
- // With type marker - typing only, no validation
487
- const result = await api.get('/file').responseType<Blob>().execute()
1130
+ // Or use soft mode
1131
+ const api = IgniterCaller.create()
1132
+ .withSchemas(schemas, { mode: 'soft' })
1133
+ .build();
488
1134
  ```
489
1135
 
490
- ## Global Events
1136
+ ---
491
1137
 
492
- You can observe responses globally using `IgniterCallerManager.on()`:
1138
+ ### Error: Cache not invalidating
493
1139
 
494
- ```ts
495
- import { IgniterCallerManager } from '@igniter-js/caller'
1140
+ **Cause:** Cache key doesn't match
496
1141
 
497
- const unsubscribe = IgniterCallerManager.on(/^\/users/, (result, ctx) => {
498
- console.log(`[${ctx.method}] ${ctx.url}`, {
499
- ok: !result.error,
500
- status: result.status,
501
- })
502
- })
1142
+ **Solution:**
503
1143
 
504
- // later
505
- unsubscribe()
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
- ## Observability (Telemetry)
1152
+ ---
1153
+
1154
+ ### Performance: Slow requests
509
1155
 
510
- ```ts
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
- const telemetry = IgniterTelemetry.create()
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
- .withBaseUrl('https://api.example.com')
522
- .withTelemetry(telemetry)
523
- .build()
1169
+ .withStore(redisAdapter)
1170
+ .build();
524
1171
  ```
525
1172
 
526
- ## Error Handling
1173
+ ---
527
1174
 
528
- All predictable failures return an `IgniterCallerError` with stable error codes.
1175
+ ### Type Inference: Not working
529
1176
 
530
- ```ts
531
- import { IgniterCallerError } from '@igniter-js/caller'
1177
+ **Cause:** Schema path doesn't match request URL
532
1178
 
533
- const result = await api.get('/users').execute()
1179
+ **Solution:**
534
1180
 
535
- if (result.error) {
536
- if (IgniterCallerError.is(result.error)) {
537
- console.error(result.error.code, result.error.operation)
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
- throw result.error
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
- // Response includes status and headers
543
- console.log(result.status) // 200
544
- console.log(result.headers?.get('x-request-id'))
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
- ## API Reference
1258
+ ### Express.js
548
1259
 
549
- ### `IgniterCaller.create()`
1260
+ ```typescript
1261
+ import express from 'express';
1262
+ import { IgniterCaller } from '@igniter-js/caller';
550
1263
 
551
- Creates a new caller builder.
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
- ### Builder Methods
1294
+ ---
554
1295
 
555
- | Method | Description |
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
- ### Request Methods
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
- | Method | Description |
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
- ### Request Builder Methods
1312
+ ## 📄 License
581
1313
 
582
- | Method | Description |
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
- ### Static Methods
1316
+ ---
597
1317
 
598
- | Method | Description |
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
- ## Contributing
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
- Contributions are welcome! Please see the main [CONTRIBUTING.md](https://github.com/felipebarcelospro/igniter-js/blob/main/CONTRIBUTING.md) for details.
1325
+ ---
609
1326
 
610
- ## License
1327
+ ## 💬 Community & Support
611
1328
 
612
- MIT License - see [LICENSE](https://github.com/felipebarcelospro/igniter-js/blob/main/LICENSE) for details.
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
- ## Links
1334
+ ---
615
1335
 
616
- - **Documentation:** https://igniterjs.com/docs
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**