npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@igniter-js/caller

v0.1.58

Published

Type-safe HTTP client for Igniter.js with interceptors, retries, caching, and StandardSchema validation

Downloads

38

Readme

@igniter-js/caller

npm version License: MIT TypeScript Node.js Bun

End-to-end type-safe HTTP client
Built on fetch with interceptors, retries, caching, schema validation, and full observability.

Quick StartDocumentationExamplesAPI Reference


✨ Why @igniter-js/caller?

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:

  • End-to-end type safety — Catch API mismatches at build time, not in production
  • Zero configuration — Works anywhere fetch works (Node 18+, Bun, Deno, browsers)
  • Production resilience — Retries, timeouts, fallbacks, and caching built-in
  • Full observability — Telemetry, logging, and global events for every request
  • Schema validation — Runtime type checking with Zod, Valibot, or any StandardSchemaV1 library
  • Developer experience — Fluent API, autocomplete everywhere, zero boilerplate

🚀 Quick Start

Installation

# Using npm
npm install @igniter-js/caller

# Using pnpm
pnpm add @igniter-js/caller

# Using yarn
yarn add @igniter-js/caller

# Using bun
bun add @igniter-js/caller

Optional dependencies:

# For schema validation (any StandardSchemaV1 library)
npm install zod

# For observability (optional)
npm install @igniter-js/telemetry

Note: @igniter-js/common is automatically installed as a dependency. zod and @igniter-js/telemetry are optional peer dependencies.

Your First API Call (60 seconds)

import { IgniterCaller } from '@igniter-js/caller';

// 1️⃣ Create the client
const api = IgniterCaller.create()
  .withBaseUrl('https://api.github.com')
  .withHeaders({ 
    'Accept': 'application/vnd.github+json',
    'X-GitHub-Api-Version': '2022-11-28'
  })
  .build();

// 2️⃣ Make a request
const result = await api.get('/users/octocat').execute();

// 3️⃣ Handle the response
if (result.error) {
  console.error('Request failed:', result.error.message);
} else {
  console.log('User:', result.data);
}

✅ Success! You just made a type-safe HTTP request with zero configuration.


🎯 Core Concepts

Architecture Overview

┌──────────────────────────────────────────────────────────────────┐
│                      Your Application                             │
├──────────────────────────────────────────────────────────────────┤
│  api.get('/users').params({ page: 1 }).execute()                │
└────────────┬─────────────────────────────────────────────────────┘
             │ Type-safe fluent API
             ▼
┌──────────────────────────────────────────────────────────────────┐
│              IgniterCallerBuilder (Immutable)                     │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │ Configuration:                                             │ │
│  │  - baseURL, headers, cookies                               │ │
│  │  - requestInterceptors, responseInterceptors                │ │
│  │  - store, schemas, telemetry, logger                       │ │
│  └────────────────────────────────────────────────────────────┘ │
└────────────┬─────────────────────────────────────────────────────┘
             │ .build()
             ▼
┌──────────────────────────────────────────────────────────────────┐
│              IgniterCallerManager (Runtime)                       │
│  - get/post/put/patch/delete/head() → RequestBuilder           │
│  - request() → axios-style direct execution                    │
│  - Static: batch(), on(), invalidate()                         │
└────────────┬─────────────────────────────────────────────────────┘
             │ Creates
             ▼
┌──────────────────────────────────────────────────────────────────┐
│          IgniterCallerRequestBuilder (Per-Request)                │
│  ┌────────────────────────────────────────────────────────────┐ │
│  │ Configuration:                                             │ │
│  │  - url, method, body, params, headers                      │ │
│  │  - timeout, cache, staleTime, retry                        │ │
│  │  - fallback, responseType (schema or type marker)          │ │
│  └────────────────────────────────────────────────────────────┘ │
└────────────┬─────────────────────────────────────────────────────┘
             │ .execute()
             ▼
┌──────────────────────────────────────────────────────────────────┐
│                 Execution Pipeline                                │
│  1. Cache Check (if staleTime set)                              │
│  2. Request Interceptors                                         │
│  3. Request Validation (if schema defined)                       │
│  4. Fetch with Retry Logic                                       │
│  5. Response Parsing (Content-Type auto-detect)                  │
│  6. Response Validation (if schema defined)                      │
│  7. Response Interceptors                                        │
│  8. Cache Store (if successful)                                  │
│  9. Fallback (if failed and fallback set)                        │
│  10. Telemetry Emission                                         │
│  11. Global Event Emission                                       │
└──────────────────────────────────────────────────────────────────┘

Key Abstractions

  • Builder → Immutable configuration (.withHeaders(), .withSchemas())
  • Manager → Operational HTTP client instance (.get(), .post())
  • RequestBuilder → Per-request fluent API (.body(), .retry(), .execute())
  • Interceptors → Request/Response transformation pipeline
  • Schemas → Type inference + runtime validation (StandardSchemaV1)
  • Cache → In-memory or store-based (Redis, etc.)
  • Events → Global observation for logging/telemetry

📖 Usage Examples

Basic Usage

import { IgniterCaller } from '@igniter-js/caller';

const api = IgniterCaller.create()
  .withBaseUrl('https://api.example.com')
  .withHeaders({ Authorization: `Bearer ${process.env.API_TOKEN}` })
  .build();

// GET request
const users = await api.get('/users').execute();

// POST request with body
const newUser = await api
  .post('/users')
  .body({ name: 'John Doe', email: '[email protected]' })
  .execute();

// PUT request with params
const updated = await api
  .put('/users/:id')
  .params({ id: '123' })
  .body({ name: 'Jane Doe' })
  .execute();

// DELETE request
const deleted = await api.delete('/users/123').execute();

// Check for errors
if (users.error) {
  console.error('Request failed:', users.error.message);
  throw users.error;
}

console.log('Users:', users.data);

Query Parameters

// Using .params()
const result = await api
  .get('/search')
  .params({ q: 'typescript', page: 1, limit: 10 })
  .execute();

// GET with body (auto-converted to query params)
const result = await api
  .get('/search')
  .body({ q: 'typescript', page: 1 })
  .execute();
// Becomes: GET /search?q=typescript&page=1

Request Headers

// Per-request headers (merged with defaults)
const result = await api
  .get('/users')
  .headers({ 'X-Custom-Header': 'value' })
  .execute();

// Override default headers
const result = await api
  .get('/public/data')
  .headers({ Authorization: '' }) // Remove auth for this request
  .execute();

Timeout & Retry

// Set timeout
const result = await api
  .get('/slow-endpoint')
  .timeout(5000) // 5 seconds
  .execute();

// Retry with exponential backoff
const result = await api
  .get('/unreliable-endpoint')
  .retry(3, {
    baseDelay: 500,
    backoff: 'exponential',
    retryOnStatus: [408, 429, 500, 502, 503, 504],
  })
  .execute();

Caching

// In-memory cache
const result = await api
  .get('/users')
  .stale(60_000) // Cache for 60 seconds
  .execute();

// Custom cache key
const result = await api
  .get('/users')
  .cache({}, 'custom-cache-key')
  .stale(60_000)
  .execute();

// Store-based caching (Redis, etc.)
const api = IgniterCaller.create()
  .withStore(redisAdapter, {
    ttl: 3600,
    keyPrefix: 'api:',
  })
  .build();

const result = await api
  .get('/users')
  .stale(300_000) // 5 minutes
  .execute();

Fallback Values

// Provide fallback if request fails
const result = await api
  .get('/optional-data')
  .fallback(() => ({ default: 'value' }))
  .execute();

// result.data will be fallback value if request fails
// result.error will still contain the original error

axios-Style Direct Requests

// Using .request() method
const result = await api.request({
  method: 'POST',
  url: '/users',
  body: { name: 'John' },
  headers: { 'X-Custom': 'value' },
  timeout: 5000,
  retry: { maxAttempts: 3, backoff: 'exponential' },
  staleTime: 30_000,
});

Interceptors

const api = IgniterCaller.create()
  .withBaseUrl('https://api.example.com')
  
  // Request interceptor (modify before sending)
  .withRequestInterceptor(async (request) => {
    return {
      ...request,
      headers: {
        ...request.headers,
        'X-Request-ID': crypto.randomUUID(),
        'X-Timestamp': new Date().toISOString(),
      },
    };
  })
  
  // Response interceptor (transform after receiving)
  .withResponseInterceptor(async (response) => {
    // Normalize empty responses
    if (response.data === '') {
      return { ...response, data: null as any };
    }
    
    // Add custom metadata
    return {
      ...response,
      metadata: {
        cached: response.headers?.get('X-Cache') === 'HIT',
        duration: parseInt(response.headers?.get('X-Duration') || '0'),
      },
    };
  })
  
  .build();

Schema Validation (Type-Safe)

import { IgniterCaller, IgniterCallerSchema } from '@igniter-js/caller';
import { z } from 'zod';

// Define schemas
const UserSchema = z.object({
  id: z.string(),
  name: z.string(),
  email: z.string().email(),
});

const ErrorSchema = z.object({
  message: z.string(),
  code: z.string(),
});

// Build schema registry
const apiSchemas = IgniterCallerSchema.create()
  .schema('User', UserSchema)
  .schema('Error', ErrorSchema)
  
  .path('/users/:id', (path) =>
    path.get({
      responses: {
        200: path.ref('User').schema,
        404: path.ref('Error').schema,
      },
    })
  )
  
  .path('/users', (path) =>
    path.get({
      responses: {
        200: path.ref('User').array(),
      },
    })
    .post({
      request: z.object({
        name: z.string(),
        email: z.string().email(),
      }),
      responses: {
        201: path.ref('User').schema,
        400: path.ref('Error').schema,
      },
    })
  )
  
  .build();

// Create typed client
const api = IgniterCaller.create()
  .withBaseUrl('https://api.example.com')
  .withSchemas(apiSchemas, { mode: 'strict' })
  .build();

// Full type inference!
const result = await api.get('/users/:id')
  .params({ id: '123' }) // ✅ params typed from path pattern
  .execute();

// ✅ result.data is User | undefined (typed from schema)
console.log(result.data?.name);

// POST with typed body
const created = await api.post('/users')
  .body({ name: 'John', email: '[email protected]' }) // ✅ body is typed
  .execute();

// ✅ created.data is User | undefined

Global Events

import { IgniterCallerManager } from '@igniter-js/caller';

// Listen to all requests
const unsubscribe = IgniterCallerManager.on(/.*/, (result, ctx) => {
  console.log(`[${ctx.method}] ${ctx.url}`, {
    status: result.status,
    success: !result.error,
    duration: Date.now() - ctx.timestamp,
  });
});

// Listen to specific paths
IgniterCallerManager.on(/^\/users/, (result, ctx) => {
  if (result.error) {
    console.error('User API failed:', result.error.message);
  }
});

// Listen to exact URL
IgniterCallerManager.on('/auth/login', (result, ctx) => {
  if (!result.error) {
    console.log('User logged in successfully');
  }
});

// Cleanup listener
unsubscribe();

Typed Mocking

import { IgniterCaller, IgniterCallerMock } from '@igniter-js/caller';
import { z } from 'zod';

const schemas = {
  '/users/:id': {
    GET: {
      responses: {
        200: z.object({ id: z.string(), name: z.string() }),
      },
    },
  },
  '/users': {
    POST: {
      request: z.object({ name: z.string() }),
      responses: {
        201: z.object({ id: z.string(), name: z.string() }),
      },
    },
  },
};

// Create mock
const mock = IgniterCallerMock.create()
  .withSchemas(schemas)
  
  // Static response
  .mock('/users/:id', {
    GET: {
      response: { id: 'user_123', name: 'John Doe' },
      status: 200,
    },
  })
  
  // Dynamic response
  .mock('/users', {
    POST: (request) => ({
      response: {
        id: crypto.randomUUID(),
        name: request.body.name,
      },
      status: 201,
      delayMs: 150, // Simulate network delay
    }),
  })
  
  .build();

// Create API with mock
const api = IgniterCaller.create()
  .withSchemas(schemas)
  .withMock({ enabled: true, mock })
  .build();

// All requests use mock
const user = await api.get('/users/:id').params({ id: '123' }).execute();
console.log(user.data); // { id: 'user_123', name: 'John Doe' }

🌍 Real-World Examples

Example 1: E-Commerce Product Catalog

import { IgniterCaller } from '@igniter-js/caller';
import { z } from 'zod';

const ProductSchema = z.object({
  id: z.string(),
  name: z.string(),
  price: z.number(),
  inStock: z.boolean(),
  images: z.array(z.string().url()),
});

const api = IgniterCaller.create()
  .withBaseUrl('https://shop-api.example.com')
  .withHeaders({ 'X-API-Key': process.env.SHOP_API_KEY! })
  .build();

// Fetch products with caching
async function getProducts(category?: string) {
  const result = await api
    .get('/products')
    .params(category ? { category } : {})
    .responseType(z.object({
      products: z.array(ProductSchema),
      total: z.number(),
    }))
    .stale(300_000) // 5 minutes
    .execute();

  if (result.error) {
    throw new Error(`Failed to fetch products: ${result.error.message}`);
  }

  return result.data;
}

// Search with debouncing
let searchAbortController: AbortController | null = null;

async function searchProducts(query: string) {
  // Cancel previous search
  searchAbortController?.abort();
  searchAbortController = new AbortController();

  const result = await api
    .get('/products/search')
    .params({ q: query })
    .timeout(3000)
    .execute();

  return result.data?.products || [];
}

// Usage
const products = await getProducts('electronics');
console.log(`Found ${products.total} products`);

Example 2: Payment Processing with Retries

import { IgniterCaller } from '@igniter-js/caller';

const api = IgniterCaller.create()
  .withBaseUrl('https://payments-api.example.com')
  .withHeaders({
    'X-API-Key': process.env.PAYMENT_API_KEY!,
    'Content-Type': 'application/json',
  })
  .build();

async function processPayment(payment: {
  amount: number;
  currency: string;
  recipient: { accountNumber: string };
}) {
  const result = await api
    .post('/payments')
    .body(payment)
    .timeout(10_000) // 10 seconds
    .retry(3, {
      baseDelay: 500,
      backoff: 'exponential',
      retryOnStatus: [503, 504], // Only retry on server errors
    })
    .fallback(() => ({
      id: 'fallback',
      status: 'pending',
      message: 'Payment queued for retry',
    }))
    .execute();

  if (result.error) {
    console.error('Payment failed:', result.error.message);
    // Log to monitoring service
    throw result.error;
  }

  return result.data;
}

Example 3: Real-Time Analytics Dashboard

import { IgniterCaller, IgniterCallerManager } from '@igniter-js/caller';

const api = IgniterCaller.create()
  .withBaseUrl('https://analytics-api.example.com')
  .build();

// Global event listener for monitoring
IgniterCallerManager.on(/^\/metrics/, (result, ctx) => {
  if (!result.error) {
    console.log(`Metrics fetched in ${Date.now() - ctx.timestamp}ms`);
  }
});

// Polling with cache
async function startMetricsPolling(intervalMs: number) {
  const poll = async () => {
    const result = await api
      .get('/metrics')
      .params({
        start: new Date(Date.now() - 300_000).toISOString(),
        end: new Date().toISOString(),
      })
      .stale(30_000) // 30 seconds
      .execute();

    if (!result.error) {
      updateDashboard(result.data);
    }
  };

  poll(); // Initial fetch
  return setInterval(poll, intervalMs);
}

const pollInterval = await startMetricsPolling(30_000);

Example 4: Multi-Tenant SaaS API Client

import { IgniterCaller } from '@igniter-js/caller';

function createTenantAPI(tenantId: string, apiKey: string) {
  return IgniterCaller.create()
    .withBaseUrl('https://saas-api.example.com')
    .withHeaders({
      'X-Tenant-ID': tenantId,
      'Authorization': `Bearer ${apiKey}`,
    })
    .build();
}

const tenant1API = createTenantAPI('tenant_1', process.env.TENANT_1_KEY!);
const tenant2API = createTenantAPI('tenant_2', process.env.TENANT_2_KEY!);

// Isolated requests per tenant
const tenant1Users = await tenant1API.get('/users').execute();
const tenant2Users = await tenant2API.get('/users').execute();

Example 5: GraphQL-Style Batch Requests

import { IgniterCallerManager } from '@igniter-js/caller';

async function fetchDashboardData() {
  const [users, posts, comments] = await IgniterCallerManager.batch([
    api.get('/users').params({ limit: 10 }).execute(),
    api.get('/posts').params({ limit: 20 }).execute(),
    api.get('/comments').params({ limit: 50 }).execute(),
  ]);

  return {
    users: users.data,
    posts: posts.data,
    comments: comments.data,
  };
}

📚 API Reference

IgniterCaller (Main Builder)

The main entry point for creating an HTTP client.

class IgniterCallerBuilder<TSchemas> {
  static create(): IgniterCallerBuilder<{}>
  
  withBaseUrl(url: string): this
  withHeaders(headers: Record<string, string>): this
  withCookies(cookies: Record<string, string>): this
  withLogger(logger: IgniterLogger): this
  withRequestInterceptor(interceptor: RequestInterceptor): this
  withResponseInterceptor(interceptor: ResponseInterceptor): this
  withStore(store: StoreAdapter, options?: StoreOptions): this
  withSchemas<T>(schemas: T, validation?: ValidationOptions): Builder<T>
  withTelemetry(telemetry: TelemetryManager): this
  withMock(config: MockConfig): this
  
  build(): IgniterCallerManager<TSchemas>
}

Methods:

| Method | Parameters | Returns | Description | |--------|------------|---------|-------------| | create() | None | Builder<{}> | Static factory for new builder | | withBaseUrl() | url: string | this | Set base URL prefix for all requests | | withHeaders() | headers: Record<string, string> | this | Merge default headers into every request | | withCookies() | cookies: Record<string, string> | this | Set default cookies (sent as Cookie header) | | withLogger() | logger: IgniterLogger | this | Attach logger for request lifecycle logging | | withRequestInterceptor() | interceptor: Function | this | Add request modifier (runs before fetch) | | withResponseInterceptor() | interceptor: Function | this | Add response transformer (runs after fetch) | | withStore() | store: Adapter, options? | this | Configure persistent cache (Redis, etc.) | | withSchemas() | schemas: Map, validation? | Builder<T> | Enable type inference + validation | | withTelemetry() | telemetry: Manager | this | Connect to telemetry system | | withMock() | config: MockConfig | this | Enable mock mode for testing | | build() | None | Manager | Build the operational client instance |

Example:

const api = IgniterCaller.create()
  .withBaseUrl('https://api.example.com')
  .withHeaders({ Authorization: 'Bearer token' })
  .withStore(redisAdapter)
  .withSchemas(schemas)
  .build();

IgniterCallerManager (HTTP Client)

The operational HTTP client instance for making requests.

class IgniterCallerManager<TSchemas> {
  // HTTP Methods
  get<T>(url?: string): RequestBuilder<T>
  post<T>(url?: string): RequestBuilder<T>
  put<T>(url?: string): RequestBuilder<T>
  patch<T>(url?: string): RequestBuilder<T>
  delete<T>(url?: string): RequestBuilder<T>
  head<T>(url?: string): RequestBuilder<T>
  
  // Direct execution (axios-style)
  request<T>(options: DirectRequestOptions): Promise<ApiResponse<T>>
  
  // Static methods
  static on(pattern: string | RegExp, callback: EventCallback): () => void
  static off(pattern: string | RegExp, callback?: EventCallback): void
  static invalidate(key: string): Promise<void>
  static invalidatePattern(pattern: string): Promise<void>
  static batch<T extends Promise<any>[]>(requests: T): Promise<AwaitedArray<T>>
}

Methods:

| Method | Arguments | Returns | Description | |--------|-----------|---------|-------------| | get() | url?: string | RequestBuilder | Create GET request | | post() | url?: string | RequestBuilder | Create POST request | | put() | url?: string | RequestBuilder | Create PUT request | | patch() | url?: string | RequestBuilder | Create PATCH request | | delete() | url?: string | RequestBuilder | Create DELETE request | | head() | url?: string | RequestBuilder | Create HEAD request | | request() | options: DirectRequestOptions | Promise<ApiResponse> | Execute request directly | | on() | pattern, callback | unsubscribe: Function | Register global event listener | | off() | pattern, callback? | void | Remove event listener(s) | | invalidate() | key: string | Promise<void> | Invalidate specific cache entry | | invalidatePattern() | pattern: string | Promise<void> | Invalidate cache by pattern | | batch() | requests: Promise[] | Promise<Results[]> | Execute requests in parallel |


RequestBuilder (Fluent Request API)

Per-request configuration builder.

class IgniterCallerRequestBuilder<TResponse> {
  url(url: string): this
  body<T>(body: T): this
  params<T>(params: T): this
  headers(headers: Record<string, string>): this
  timeout(ms: number): this
  cache(cache: CacheInit, key?: string): this
  stale(ms: number): this
  retry(attempts: number, options?: RetryOptions): this
  fallback<T>(fn: () => T): this
  responseType<T>(schema?: StandardSchemaV1<T>): RequestBuilder<T>
  
  execute(): Promise<ApiResponse<TResponse>>
}

Methods:

| Method | Parameters | Returns | Description | |--------|------------|---------|-------------| | url() | url: string | this | Set request URL | | body() | body: any | this | Set request body (JSON, FormData, Blob) | | params() | params: Record<string, any> | this | Set query parameters | | headers() | headers: Record<string, string> | this | Merge additional headers | | timeout() | ms: number | this | Set request timeout | | cache() | cache: CacheInit, key?: string | this | Set cache strategy | | stale() | ms: number | this | Set cache stale time | | retry() | attempts: number, options? | this | Configure retry behavior | | fallback() | fn: () => T | this | Provide fallback value on error | | responseType() | schema?: StandardSchemaV1 | Builder<T> | Set expected response type | | execute() | None | Promise<ApiResponse> | Execute the request |


Types

ApiResponse

interface IgniterCallerApiResponse<TData> {
  data?: TData;
  error?: IgniterCallerError;
  status?: number;
  headers?: Headers;
}

RetryOptions

interface IgniterCallerRetryOptions {
  maxAttempts: number;
  baseDelay?: number;
  backoff?: 'linear' | 'exponential';
  retryOnStatus?: number[];
}

ValidationOptions

interface IgniterCallerSchemaValidationOptions {
  mode?: 'strict' | 'soft' | 'off';
  onValidationError?: (error: ValidationError) => void;
}

🔧 Configuration

Store Adapter

Configure persistent caching with Redis or other stores:

interface IgniterCallerStoreAdapter<TClient = any> {
  client: TClient | null;
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ttl?: number): Promise<void>;
  delete(key: string): Promise<void>;
  has(key: string): Promise<boolean>;
}

interface IgniterCallerStoreOptions {
  ttl?: number;
  keyPrefix?: string;
}

Example:

import { IgniterCaller } from '@igniter-js/caller';

const redisAdapter: IgniterCallerStoreAdapter = {
  client: redis,
  async get(key) { return await redis.get(key); },
  async set(key, value, ttl) { await redis.setex(key, ttl || 3600, value); },
  async delete(key) { await redis.del(key); },
  async has(key) { return (await redis.exists(key)) === 1; },
};

const api = IgniterCaller.create()
  .withStore(redisAdapter, {
    ttl: 3600,
    keyPrefix: 'api:',
  })
  .build();

Schema Validation

Enable runtime validation with any StandardSchemaV1 library:

import { z } from 'zod';

const api = IgniterCaller.create()
  .withSchemas(schemas, {
    mode: 'strict', // 'strict' | 'soft' | 'off'
    onValidationError: (error) => {
      console.error('Validation failed:', error);
    },
  })
  .build();

Modes:

  • strict: Throw on validation failure (default)
  • soft: Log error and continue
  • off: Skip validation

🧪 Testing

Unit Testing with Mock Adapter

import { describe, it, expect } from 'vitest';
import { IgniterCaller, IgniterCallerMock } from '@igniter-js/caller';
import { MockCallerStoreAdapter } from '@igniter-js/caller/adapters';

describe('API Client', () => {
  const mock = IgniterCallerMock.create()
    .mock('/users/:id', {
      GET: (request) => ({
        response: { id: request.params.id, name: 'Test User' },
        status: 200,
      }),
    })
    .build();

  const api = IgniterCaller.create()
    .withMock({ enabled: true, mock })
    .build();

  it('should fetch user', async () => {
    const result = await api.get('/users/:id').params({ id: '123' }).execute();
    
    expect(result.error).toBeUndefined();
    expect(result.data).toEqual({ id: '123', name: 'Test User' });
  });

  it('should handle errors', async () => {
    const mock = IgniterCallerMock.create()
      .mock('/error', {
        GET: { response: null, status: 500 },
      })
      .build();

    const api = IgniterCaller.create()
      .withMock({ enabled: true, mock })
      .build();

    const result = await api.get('/error').execute();
    
    expect(result.error).toBeDefined();
  });
});

Integration Testing

import { IgniterCaller } from '@igniter-js/caller';

describe('Integration: Real API', () => {
  const api = IgniterCaller.create()
    .withBaseUrl(process.env.TEST_API_URL!)
    .build();

  it('should fetch users from real API', async () => {
    const result = await api.get('/users').execute();
    
    expect(result.error).toBeUndefined();
    expect(Array.isArray(result.data)).toBe(true);
  });
});

🎨 Best Practices

✅ Do

// ✅ Use immutable builders
const api = IgniterCaller.create()
  .withBaseUrl('...')
  .withHeaders({ ... })
  .build();

// ✅ Handle errors explicitly
const result = await api.get('/users').execute();
if (result.error) {
  console.error(result.error);
  throw result.error;
}

// ✅ Use schema validation for type safety
const api = IgniterCaller.create()
  .withSchemas(schemas, { mode: 'strict' })
  .build();

// ✅ Cache expensive requests
const result = await api
  .get('/expensive')
  .stale(300_000) // 5 minutes
  .execute();

// ✅ Use retry for transient failures
const result = await api
  .get('/unreliable')
  .retry(3, { backoff: 'exponential' })
  .execute();

// ✅ Provide fallbacks for optional data
const result = await api
  .get('/optional')
  .fallback(() => defaultValue)
  .execute();

❌ Don't

// ❌ Don't mutate builder state
const builder = IgniterCaller.create();
builder.state.baseURL = 'https://api.example.com'; // ❌ Won't work

// ❌ Don't ignore errors
const result = await api.get('/users').execute();
console.log(result.data); // ❌ Might be undefined

// ❌ Don't skip validation in production
const api = IgniterCaller.create()
  .withSchemas(schemas, { mode: 'off' }) // ❌ Risky
  .build();

// ❌ Don't cache mutations
const result = await api
  .post('/users')
  .stale(60_000) // ❌ Don't cache POST/PUT/PATCH/DELETE
  .execute();

// ❌ Don't retry non-idempotent operations
const result = await api
  .post('/payments')
  .retry(3) // ❌ Might duplicate payment
  .execute();

🚨 Troubleshooting

Error: Request timeout

Cause: Request took longer than configured timeout

Solution:

// Increase timeout
const result = await api
  .get('/slow-endpoint')
  .timeout(30_000) // 30 seconds
  .execute();

Error: Validation failed

Cause: Response doesn't match schema

Solution:

// Check schema definition
const UserSchema = z.object({
  id: z.string(),
  name: z.string(), // ❌ API returns `username`
});

// Fix schema
const UserSchema = z.object({
  id: z.string(),
  username: z.string(), // ✅ Matches API
});

// Or use soft mode
const api = IgniterCaller.create()
  .withSchemas(schemas, { mode: 'soft' })
  .build();

Error: Cache not invalidating

Cause: Cache key doesn't match

Solution:

// Ensure consistent cache keys
const result1 = await api.get('/users').cache({}, 'users-list').execute();

// Later, invalidate with same key
await IgniterCallerManager.invalidate('users-list');

Performance: Slow requests

Diagnosis: No caching or retries

Solution:

// Enable caching for read-heavy endpoints
const result = await api
  .get('/heavy-computation')
  .stale(600_000) // 10 minutes
  .execute();

// Use store-based cache for persistence
const api = IgniterCaller.create()
  .withStore(redisAdapter)
  .build();

Type Inference: Not working

Cause: Schema path doesn't match request URL

Solution:

// ❌ Schema path doesn't match
const schemas = {
  '/users': { GET: { responses: { 200: UserSchema } } }
};

const result = await api.get('/users/list').execute(); // ❌ No match

// ✅ Fix schema or URL
const schemas = {
  '/users/list': { GET: { responses: { 200: UserSchema } } }
};

const result = await api.get('/users/list').execute(); // ✅ Typed

🔗 Framework Integration

Next.js (App Router)

// lib/api.ts
import { IgniterCaller } from '@igniter-js/caller';

export const api = IgniterCaller.create()
  .withBaseUrl(process.env.NEXT_PUBLIC_API_URL!)
  .build();

// app/users/page.tsx
import { api } from '@/lib/api';

export default async function UsersPage() {
  const result = await api.get('/users').execute();
  
  if (result.error) {
    throw new Error('Failed to fetch users');
  }
  
  return (
    <div>
      {result.data.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  );
}

React with TanStack Query

import { useQuery } from '@tanstack/react-query';
import { api } from './api';

function useUsers() {
  return useQuery({
    queryKey: ['users'],
    queryFn: async () => {
      const result = await api.get('/users').execute();
      if (result.error) throw result.error;
      return result.data;
    },
  });
}

function Users() {
  const { data, isLoading, error } = useUsers();
  
  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;
  
  return <ul>{data.map(...)}</ul>;
}

Express.js

import express from 'express';
import { IgniterCaller } from '@igniter-js/caller';

const app = express();
const api = IgniterCaller.create()
  .withBaseUrl('https://external-api.example.com')
  .build();

app.get('/proxy/users', async (req, res) => {
  const result = await api.get('/users').execute();
  
  if (result.error) {
    return res.status(result.status || 500).json({
      error: result.error.message,
    });
  }
  
  res.json(result.data);
});

📊 Performance Tips

  1. Use caching aggressively for read-heavy endpoints
  2. Enable store-based caching (Redis) for distributed systems
  3. Batch parallel requests with IgniterCallerManager.batch()
  4. Set appropriate timeouts to fail fast
  5. Use retry with exponential backoff for transient failures
  6. Minimize interceptor overhead (avoid heavy computation)
  7. Enable compression via headers (Accept-Encoding: gzip)

🤝 Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

Development Setup

git clone https://github.com/felipebarcelospro/igniter-js.git
cd igniter-js/packages/caller
npm install
npm run build
npm test

📄 License

MIT © Felipe Barcelos


🔗 Related Packages


💬 Community & Support


Built with ❤️ by the Igniter.js team