Request Builder
Master the fluent API for building type-safe HTTP requests. Learn how to configure URLs, bodies, headers, timeouts, caching, retries, and more with chainable methods.
Overview
The Request Builder is the heart of Caller's developer experience. Instead of passing complex configuration objects, you chain methods together to build your request step by step. Each method returns the builder, enabling a fluid, readable syntax.
const response = await api.post('/users')
.body({ name: 'John', email: 'john@example.com' })
.headers({ 'X-Idempotency-Key': crypto.randomUUID() })
.timeout(5000)
.retry(3, { backoff: 'exponential' })
.execute();Every method in the chain is optional. Use only what you need.
Starting a Request
Requests begin by calling an HTTP method on your API client:
// All methods return an IgniterCallerRequestBuilder
const getBuilder = api.get('/users');
const postBuilder = api.post('/users');
const putBuilder = api.put('/users/:id');
const patchBuilder = api.patch('/users/:id');
const deleteBuilder = api.delete('/users/:id');
const headBuilder = api.head('/status');The HTTP method is locked once you start — you can't change a GET to a POST later. This ensures type safety throughout the chain.
URL Parameters
The :id syntax lets you define parameterized URLs. Fill them in with .params(). Extra params become query string parameters.
Core Methods
Resilience Methods
Response Methods
Executing Requests
The .execute() method sends the request and returns a Promise:
const result = await api.get('/users').execute();Response Envelope
| Property | Type | Description |
|---|---|---|
data | T | undefined | Parsed response body. Undefined on error. |
error | IgniterError | undefined | Error object. Undefined on success. |
status | number | undefined | HTTP status code. |
headers | Headers | undefined | Response headers. |
Error Handling Patterns
// Pattern 1: Guard clause (most common)
const { data, error } = await api.get('/users').execute();
if (error) {
console.error(`${error.code}: ${error.message}`);
return;
}
console.log(data); // TypeScript narrows data to T
// Pattern 2: Fallback for safe defaults
const { data } = await api.get('/optional-data')
.fallback(() => ({ default: true }))
.execute();
// data is always defined
// Pattern 3: Match on status
const result = await api.post('/users').body(payload).execute();
switch (result.status) {
case 201: return result.data;
case 400: throw new ValidationError(result.error!);
case 409: throw new ConflictError(result.error!);
default: throw result.error;
}Execution Pipeline
When you call .execute(), Caller runs through this pipeline:
- Cache Check — If
.stale()is set and a cached response exists, return it immediately - Request Interceptors — Run all registered request interceptors in order
- Schema Validation (Request) — If schemas are configured, validate the request body
- Fetch with Retry — Send the request; retry on failure according to
.retry()config - Response Parsing — Auto-detect content type and parse (JSON, text, blob, etc.)
- Schema Validation (Response) — If schemas are configured and content type is validatable, validate the response
- Response Interceptors — Run all registered response interceptors in order
- Cache Store — If
.stale()is set and the request succeeded, store in cache - Fallback — If the request failed and
.fallback()is configured, return the fallback value - Telemetry Emission — Emit success/error telemetry events
Complete Example
Here's a production-ready example combining multiple builder methods:
import { api } from '@/lib/api';
import { IgniterCallerError } from '@igniter-js/caller';
async function createOrder(items: CartItem[], userId: string) {
const { data, error, status } = await api.post('/orders')
.body({
userId,
items: items.map(item => ({
productId: item.id,
quantity: item.quantity,
})),
metadata: {
source: 'web',
timestamp: new Date().toISOString(),
},
})
.headers({
'X-Idempotency-Key': `order-${userId}-${Date.now()}`,
})
.timeout(10_000)
.retry(2, {
backoff: 'linear',
retryOnStatus: [502, 503],
})
.execute();
if (error) {
if (status === 400) {
return { success: false, message: 'Invalid order data', details: error.details };
}
if (status === 409) {
return { success: false, message: 'Duplicate order' };
}
throw error; // Unexpected error — propagate
}
return { success: true, orderId: data!.id };
}Best Practices
Builder Method Order
While methods can be called in any order, we recommend this sequence for readability:
.url()— URL configuration (if not set by HTTP method).params()— Path and query parameters.body()— Request payload.headers()— Custom headers.timeout()— Timing configuration.retry(),.cache(),.stale()— Resilience.responseType()— Response typing.execute()— Always last
Common Mistakes
- Don't call
.execute()before.retry()— the request executes immediately - Don't use
.query()— it doesn't exist. Use.params()for both path and query params - Do use
.fallback(() => value)— it's a factory function, not a plain value - Do check for
errorbefore accessingdata—dataisundefinedon failure