Quick Start
Make your first type-safe HTTP request with Caller in under 2 minutes. Step-by-step guide covering installation, client creation, and common patterns.
This guide walks you through creating your first Caller client and making requests. By the end, you'll have a working API client with type-safe responses.
Install Caller
npm install @igniter-js/callerpnpm add @igniter-js/calleryarn add @igniter-js/callerbun add @igniter-js/callerCreate Your Client
Create a shared API client instance. This is typically done once at application startup:
import { IgniterCaller } from '@igniter-js/caller';
export const api = IgniterCaller.create()
.withBaseUrl('https://jsonplaceholder.typicode.com')
.withHeaders({
'Content-Type': 'application/json',
})
.build();Minimal setup
For the simplest case, you can skip all configuration:
const api = IgniterCaller.create().build();This works anywhere fetch is available.
Make Your First GET Request
import { api } from './lib/api';
async function fetchUsers() {
const result = await api.get('/users').execute();
if (result.error) {
console.error('Failed to fetch users:', result.error.message);
return [];
}
// result.data is the parsed response body
console.log(`Fetched ${result.data.length} users`);
return result.data;
}Never-throw by default
Caller never throws on HTTP error statuses (like 404 or 500). Instead, it returns an error object in the result envelope. It only throws on network failures or configuration errors.
Add Query Parameters
Use .params() to add both path parameters and query string parameters:
// Path parameters — :id is replaced
const user = await api.get('/users/:id')
.params({ id: '1' })
.execute();
// Requests: GET /users/1
// Query parameters — added to the URL
const filtered = await api.get('/users')
.params({ page: 1, limit: 10, status: 'active' })
.execute();
// Requests: GET /users?page=1&limit=10&status=activeAny params not matching path segments are automatically appended as query string parameters.
Send Data with POST
async function createUser(name: string, email: string) {
const result = await api.post('/users')
.body({ name, email })
.execute();
if (result.error) {
// Handle validation errors, conflicts, etc.
if (result.status === 409) {
throw new Error('User already exists');
}
throw result.error;
}
// result.status is 201 for successful creation
console.log('Created user:', result.data.id);
return result.data;
}Update and Delete Resources
// PUT — replace entire resource (idempotent)
const updated = await api.put('/users/:id')
.params({ id: '1' })
.body({ name: 'Jane Doe', email: 'jane@example.com' })
.execute();
// PATCH — partial update
const patched = await api.patch('/users/:id')
.params({ id: '1' })
.body({ name: 'Jane Updated' })
.execute();
// DELETE — remove resource
const deleted = await api.delete('/users/:id')
.params({ id: '1' })
.execute();
if (!deleted.error) {
console.log('User deleted successfully');
}Adding Resilience
Production applications need to handle transient failures. Caller provides retries, timeouts, and fallbacks out of the box:
// Retry up to 3 times with exponential backoff
const result = await api.get('/external-service')
.retry(3, {
backoff: 'exponential',
baseDelay: 500,
retryOnStatus: [502, 503, 504],
})
.timeout(10000) // 10 second timeout
.fallback(() => []) // Return empty array if all retries fail
.execute();Retry defaults
By default, Caller retries on status codes 408, 429, 500, 502, 503, and 504. You can customize this with retryOnStatus.
Adding Type Safety
The real power of Caller comes from schema-based type inference. Define your API contract once and get automatic TypeScript types everywhere:
import { IgniterCaller, IgniterCallerSchema } from '@igniter-js/caller';
import { z } from 'zod';
// 1. Define your API schemas
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
const schemas = IgniterCallerSchema.create()
.path('/users/:id', (path) =>
path.get({
responses: {
200: UserSchema,
404: z.object({ message: z.string() }),
},
})
)
.path('/users', (path) =>
path.get({
responses: { 200: z.array(UserSchema) },
})
.post({
request: z.object({
name: z.string(),
email: z.string().email(),
}),
responses: {
201: UserSchema,
400: z.object({ message: z.string() }),
},
})
)
.build();
// 2. Create your typed client
const api = IgniterCaller.create()
.withBaseUrl('https://api.example.com')
.withSchemas(schemas, { mode: 'strict' })
.build();
// 3. Full type inference — no manual interfaces needed!
const user = await api.get('/users/:id')
.params({ id: '1' }) // ✅ params are typed
.execute();
// user.data is typed as { id: number; name: string; email: string } | null
const created = await api.post('/users')
.body({ name: 'John', email: 'john@example.com' }) // ✅ body is typed
.execute();
// created.data is typed as { id: number; name: string; email: string } | nullError Handling Patterns
Caller returns a { data, error } envelope. Here are the most common patterns:
// Pattern 1: Guard clause
const { data, error } = await api.get('/users').execute();
if (error) {
console.error(`${error.code}: ${error.message}`);
return;
}
// TypeScript knows data is defined here
console.log(data.length);
// Pattern 2: Inline with fallback
const { data } = await api.get('/users')
.fallback(() => [])
.execute();
// data is always defined
console.log(data.length);
// 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;
}Using with React
Caller provides first-class React hooks through the /client subpath:
import { IgniterCallerProvider } from '@igniter-js/caller/client';
import { api } from '@/lib/api';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<IgniterCallerProvider callers={{ main: api }}>
{children}
</IgniterCallerProvider>
);
}import { useIgniterCaller } from '@igniter-js/caller/client';
const useCaller = useIgniterCaller<{ main: typeof api }>();
function UserProfile({ id }: { id: string }) {
const main = useCaller('main');
const { data, isLoading, error, refetch } = main
.get('/users/:id')
.params({ id })
.useQuery({ staleTime: 30_000 });
if (isLoading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div>
<h1>{data?.name}</h1>
<p>{data?.email}</p>
<button onClick={() => refetch()}>Refresh</button>
</div>
);
}Next Steps
Now that you've made your first requests, explore the full capabilities:
Installation
Complete setup guide with all configuration options and troubleshooting
Request Builder
Master the fluent API — headers, timeouts, retries, caching, and more
Schema Validation
Define type-safe API contracts with Zod and automatic inference
React Integration
React hooks for data fetching with automatic caching and invalidation
Real-World Examples
Production-ready patterns for common API workflows
Comparison
See how Caller compares to Axios, ky, and ofetch
Introduction
A type-safe HTTP client for Igniter.js with automatic schema validation, caching, retries, and React integration. Built on native fetch.
Installation
Complete guide to installing and configuring @igniter-js/caller. Covers all package managers, TypeScript setup, optional dependencies, and environment configuration.