Skip to content

Configuration

FetchClient uses a default provider behind the scenes that manages shared configuration, cache, and state for your entire app. You don't need to create or manage providers directly - just call the configuration functions and everything works.

How It Works

When you use new FetchClient() or call functions like getJSON(), they all share the same default provider. This means:

  • Shared configuration - Set baseUrl once, use it everywhere
  • Shared cache - Cache entries are available to all clients
  • Shared middleware - Add logging once, it applies to all requests
  • Shared state - Track loading state across your entire app
ts
import { FetchClient, getJSON, setBaseUrl } from "@foundatiofx/fetchclient";

setBaseUrl("https://api.example.com");

// Both use the same configuration and cache
const client = new FetchClient();
await client.getJSON("/users");  // Uses baseUrl
await getJSON("/users");         // Same baseUrl, same cache

Basic Setup

Configure your app once at startup:

ts
// app-init.ts
import {
  setAccessTokenFunc,
  setBaseUrl,
  useMiddleware,
} from "@foundatiofx/fetchclient";

setBaseUrl("https://api.example.com");

setAccessTokenFunc(() => localStorage.getItem("token"));

useMiddleware(async (ctx, next) => {
  console.log(`→ ${ctx.request.method} ${ctx.request.url}`);
  await next();
  console.log(`← ${ctx.response?.status}`);
});

Then use the API anywhere:

ts
// anywhere.ts
import { getJSON, postJSON } from "@foundatiofx/fetchclient";

const { data: users } = await getJSON<User[]>("/users");
const { data: created } = await postJSON<User>("/users", { name: "Alice" });

Configuration Options

Base URL

ts
import { setBaseUrl } from "@foundatiofx/fetchclient";

setBaseUrl("https://api.example.com");

// Now all requests use this base URL
await getJSON("/users"); // https://api.example.com/users

Authentication

ts
import { setAccessTokenFunc } from "@foundatiofx/fetchclient";

setAccessTokenFunc(() => localStorage.getItem("token"));

// All requests automatically include Authorization: Bearer <token>

Middleware

ts
import { useMiddleware } from "@foundatiofx/fetchclient";

// Logging
useMiddleware(async (ctx, next) => {
  const start = Date.now();
  await next();
  console.log(`${ctx.request.url}: ${Date.now() - start}ms`);
});

// Error tracking
useMiddleware(async (ctx, next) => {
  try {
    await next();
  } catch (error) {
    errorTracker.capture(error);
    throw error;
  }
});

Rate Limiting

ts
import { usePerDomainRateLimit, useRateLimit } from "@foundatiofx/fetchclient";

// Global rate limit
useRateLimit({
  maxRequests: 100,
  windowSeconds: 60,
});

// Or per-domain (each domain gets its own limit)
usePerDomainRateLimit({
  maxRequests: 100,
  windowSeconds: 60,
});

Circuit Breaker

ts
import {
  useCircuitBreaker,
  usePerDomainCircuitBreaker,
} from "@foundatiofx/fetchclient";

// Global circuit breaker
useCircuitBreaker({
  failureThreshold: 5,
  openDurationMs: 30000,
});

// Or per-domain (each domain gets its own circuit)
usePerDomainCircuitBreaker({
  failureThreshold: 5,
  openDurationMs: 30000,
});

Model Validation

Validate request data before sending:

ts
import { ProblemDetails, setModelValidator } from "@foundatiofx/fetchclient";

setModelValidator(async (data) => {
  const result = schema.safeParse(data);
  if (!result.success) {
    const problem = new ProblemDetails();
    problem.errors = formatErrors(result.error);
    return problem;
  }
  return null;
});

Shared Cache

All FetchClient instances share the same cache. Use getCache() to access it:

ts
import { getCache } from "@foundatiofx/fetchclient";

// Invalidate specific entries
getCache().delete(["users", "1"]);

// Invalidate by tag
getCache().deleteByTag("users");

// Clear all cache
getCache().clear();

Loading State

Track loading state across all requests:

ts
import { getCurrentProvider } from "@foundatiofx/fetchclient";

const provider = getCurrentProvider();

// Check if any request is in progress
console.log(provider.isLoading);
console.log(provider.requestCount);

// Subscribe to loading state changes
provider.loading.on((isLoading) => {
  if (isLoading) {
    showSpinner();
  } else {
    hideSpinner();
  }
});

Complete Example

ts
// app-init.ts
import {
  getCache,
  getCurrentProvider,
  setAccessTokenFunc,
  setBaseUrl,
  useCircuitBreaker,
  useMiddleware,
  usePerDomainRateLimit,
} from "@foundatiofx/fetchclient";

// Base configuration
setBaseUrl("https://api.example.com");
setAccessTokenFunc(() => localStorage.getItem("token"));

// Logging middleware
useMiddleware(async (ctx, next) => {
  const start = Date.now();
  await next();
  console.log(
    `${ctx.request.url}: ${ctx.response?.status} (${Date.now() - start}ms)`,
  );
});

// Resilience
usePerDomainRateLimit({ maxRequests: 100, windowSeconds: 60 });
useCircuitBreaker({ failureThreshold: 5, openDurationMs: 30000 });

// Loading indicator
getCurrentProvider().loading.on((isLoading) => {
  document.body.classList.toggle("loading", isLoading);
});
ts
// user-service.ts
import { deleteJSON, getCache, getJSON, postJSON } from "@foundatiofx/fetchclient";

export async function getUsers() {
  const { data } = await getJSON<User[]>("/users", {
    cacheKey: ["users"],
    cacheDuration: 60000,
    cacheTags: ["users"],
  });
  return data;
}

export async function createUser(name: string) {
  const { data } = await postJSON<User>("/users", { name });
  getCache().deleteByTag("users"); // Invalidate user cache
  return data;
}

export async function deleteUser(id: number) {
  await deleteJSON(`/users/${id}`);
  getCache().deleteByTag("users");
}

Advanced: Custom Providers

For advanced use cases like connecting to multiple APIs with different configurations, you can create separate FetchClientProvider instances. See the API reference for details.

Released under the MIT License.