Skip to content

Fetchit API Reference

Complete API documentation for @vielzeug/fetchit.

Table of Contents

Overview

Fetchit provides two separate clients for maximum flexibility:

  1. HTTP Client (createHttpClient) – Pure HTTP operations without caching
  2. Query Client (createQueryClient) – Advanced query management with caching

Use them together or independently based on your needs.

Core Functions

createHttpClient(options?)

Creates a pure HTTP client without query management. Perfect for simple HTTP requests without caching overhead.

Parameters:

  • options?: HttpClientOptions
    • baseUrl?: string – Base URL for all requests
    • headers?: Record<string, string> – Default headers for all requests
    • timeout?: number – Request timeout in milliseconds (default: 30000)
    • dedupe?: boolean – Enable request deduplication (default: true)
    • logger?: (level, msg, meta) => void – Optional logger function for debugging

Returns: HTTP client instance with REST methods

Example:

ts
import { createHttpClient } from '@vielzeug/fetchit';

const http = createHttpClient({
  baseUrl: 'https://api.example.com',
  timeout: 10000,
  headers: { Authorization: 'Bearer token' },
  dedupe: true,
  logger: (level, msg, meta) => {
    console.log(`[${level}]`, msg, meta);
  },
});

// Direct HTTP requests (returns raw data)
const user = await http.get('/users/1');
const created = await http.post('/users', { body: newUser });

Available Methods:

  • get<T>(url, config?): Promise<T> – GET request
  • post<T>(url, config?): Promise<T> – POST request
  • put<T>(url, config?): Promise<T> – PUT request
  • patch<T>(url, config?): Promise<T> – PATCH request
  • delete<T>(url, config?): Promise<T> – DELETE request
  • request<T>(method, url, config?): Promise<T> – Custom HTTP method
  • setHeaders(headers): void – Update global headers

createQueryClient(options?)

Creates a pure query management client. Works with any HTTP client or fetch function.

Parameters:

  • options?: QueryClientOptions
    • staleTime?: number – Time before data is stale in ms (default: 0)
    • gcTime?: number – Garbage collection time in ms (default: 300000)

Returns: Query client instance

Example:

ts
import { createQueryClient, createHttpClient } from '@vielzeug/fetchit';

const queryClient = createQueryClient({
  staleTime: 5000,
  gcTime: 300000,
});

// Use with HTTP client
const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const user = await queryClient.fetch({
  queryKey: ['users', 1],
  queryFn: () => http.get('/users/1'),
});

// Or with native fetch
const data = await queryClient.fetch({
  queryKey: ['data'],
  queryFn: () => fetch('/data').then((r) => r.json()),
});

Available Methods:

Core Methods:

  • fetch<T>(options): Promise<T> – Execute a query with caching
  • mutate<TData, TVariables>(options, variables): Promise<TData> – Execute a mutation
  • prefetch<T>(options): Promise<void> – Prefetch a query
  • getData<T>(queryKey): T | undefined – Get cached data
  • setData<T>(queryKey, data | updater): void – Set/update cached data
  • getState<T>(queryKey): QueryState<T> | null – Get full query state
  • invalidate(queryKey): void – Invalidate query (supports prefix matching)
  • subscribe<T>(queryKey, listener): () => void – Subscribe to query changes (returns unsubscribe function)
  • clear(): void – Clear all cached data

Type-Safe Query Keys

While there's no built-in helper, you can create type-safe query keys using TypeScript's as const:

Example:

ts
// Define query keys manually
const queryKeys = {
  users: {
    all: () => ['users'] as const,
    detail: (id: string) => ['users', id] as const,
    list: (filters: { role?: string }) => ['users', 'list', filters] as const,
  },
  posts: {
    all: () => ['posts'] as const,
    detail: (id: number) => ['posts', id] as const,
  },
} as const;

// Type-safe usage – autocomplete works!
queryClient.fetch({
  queryKey: queryKeys.users.detail('123'),
  queryFn: () => http.get('/users/123'),
});

// Invalidate with type safety
queryClient.invalidate(queryKeys.users.all());

Query Client Methods

The Query Client provides caching and state management. Use it with any HTTP client or fetch function.

fetch<T>(options)

Execute a query with automatic caching, deduplication, and smart refetching.

Parameters:

  • options: QueryOptions<T>
    • queryKey: QueryKey – Array-based unique identifier (e.g., ['users', 1])
    • queryFn: () => Promise<T> – Function that fetches the data
    • staleTime?: number – Time in ms before data is stale (default: 0)
    • gcTime?: number – Time in ms before garbage collection (default: 300000)
    • enabled?: boolean – Whether to execute the query (default: true)
    • retry?: number | false – Number of retry attempts (default: 3)
    • retryDelay?: number | ((attempt: number) => number) – Delay between retries
    • onSuccess?: (data: T) => void – Success callback
    • onError?: (error: Error) => void – Error callback

Returns: Promise<T> – The fetched data

Example:

ts
import { createHttpClient, createQueryClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const queryClient = createQueryClient();

const user = await queryClient.fetch({
  queryKey: ['users', userId],
  queryFn: () => http.get(`/users/${userId}`),
  staleTime: 5000,
  gcTime: 300000,
  retry: 3,
  onSuccess: (data) => console.log('User loaded:', data),
});

mutate<TData, TVariables>(options, variables)

Execute a mutation (POST, PUT, PATCH, DELETE) with retry support and lifecycle callbacks.

Parameters:

  • options: MutationOptions<TData, TVariables>
    • mutationFn: (variables: TVariables) => Promise<TData> – Function to execute
    • retry?: number | false – Number of retry attempts (default: false)
    • retryDelay?: number | ((attempt: number) => number) – Delay between retries
    • onSuccess?: (data: TData, variables: TVariables) => void – Success callback
    • onError?: (error: Error, variables: TVariables) => void – Error callback
    • onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void – Always called
  • variables: TVariables – Variables to pass to mutationFn

Returns: Promise<TData> – The mutation result

Example:

ts
const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const queryClient = createQueryClient();

const newUser = await queryClient.mutate(
  {
    mutationFn: (data) => http.post<User>('/users', { body: data }),
    retry: 1,
    onSuccess: (data) => {
      console.log('User created:', data);
      queryClient.invalidate(['users']);
    },
  },
  { name: 'Alice', email: 'alice@example.com' },
);

HTTP Client Methods

All HTTP client methods return Promise<T> – the raw data directly (not wrapped in a response object).

🔥 Automatic Deduplication: All HTTP requests are automatically deduplicated by default. Concurrent identical requests (same URL, method, and body) will share the same network call. Disable per-request with { dedupe: false }.

get<T>(url, config?)

Makes a GET request and returns the data directly.

Config Options:

  • params?: Record<string, string | number | undefined> – Query parameters
  • dedupe?: boolean – Enable/disable deduplication (default: true)
  • signal?: AbortSignal – AbortSignal for request cancellation

Example:

ts
const http = createHttpClient({ baseUrl: 'https://api.example.com' });

const user = await http.get<User>('/users/1');
console.log(user.name); // Direct access

// Concurrent requests are automatically deduped
const [u1, u2, u3] = await Promise.all([http.get('/users/1'), http.get('/users/1'), http.get('/users/1')]);
// Only 1 network request made!

post<T>(url, config?)

Makes a POST request and returns the data directly.

Example:

ts
const user = await http.post<User>('/users', {
  body: { name: 'Alice', email: 'alice@example.com' },
});
console.log(user.id); // Direct access

put<T>(url, config?)

Makes a PUT request and returns the data directly.

Example:

ts
const user = await http.put<User>('/users/1', {
  body: { name: 'Alice Smith' },
});
console.log(user); // Updated user

patch<T>(url, config?)

Makes a PATCH request.

Example:

ts
const user = await http.patch<User>('/users/1', {
  body: { email: 'newemail@example.com' },
});

delete<T>(url, config?)

Makes a DELETE request.

Example:

ts
await http.delete('/users/1');

request<T>(method, url, config?)

Makes a request with a custom HTTP method. Useful for less common methods like OPTIONS, HEAD, etc.

Parameters:

  • method: string – HTTP method (e.g., 'OPTIONS', 'HEAD')
  • url: string – Request URL
  • config?: HttpRequestConfig – Request configuration

Returns: Promise<T>

Example:

ts
// OPTIONS request
const corsInfo = await http.request<CorsInfo>('OPTIONS', '/users');

// HEAD request to check if resource exists
await http.request('HEAD', '/users/1');

setHeaders(headers)

Update global headers for all future requests.

Parameters:

  • headers: Record<string, string | undefined> – Headers to set (undefined removes header)

Example:

ts
// Set auth token
http.setHeaders({ Authorization: `Bearer ${token}` });

// Remove auth token
http.setHeaders({ Authorization: undefined });

Query Client Cache Management

invalidate(queryKey)

Invalidates and removes queries from cache. Supports prefix matching!

Prefix Matching:

  • Exact match: ['users', '1'] matches only ['users', '1']
  • Prefix match: ['users'] matches ['users'], ['users', '1'], ['users', 'list', {...}], etc.

Example:

ts
// Invalidate specific user
queryClient.invalidate(['users', '1']);

// Invalidate ALL user queries (prefix matching!)
queryClient.invalidate(['users']);
// Matches: ['users'], ['users', '1'], ['users', 'list', {...}], etc.

setData<T>(queryKey, data)

Manually set or update data in the query cache.

Parameters:

  • queryKey: QueryKey – The query key
  • data: T | ((old: T | undefined) => T) – New data or updater function

Example:

ts
// Set data directly
queryClient.setData(['users', '1'], { id: '1', name: 'Alice' });

// Update existing data
queryClient.setData(['users', '1'], (old) => ({ ...old, name: 'Alice Updated' }));

getData<T>(queryKey)

Get cached data for a query.

Parameters:

  • queryKey: QueryKey – The query key

Returns: T | undefined – Cached data or undefined

Example:

ts
const user = queryClient.getData<User>(['users', '1']);
if (user) {
  console.log('Cached user:', user);
}

getState<T>(queryKey)

Get the full state of a query including status, error, and metadata.

Parameters:

  • queryKey: QueryKey – The query key

Returns: QueryState<T> | null – Query state object or null if not found

QueryState Type:

ts
{
  data: T | undefined;
  error: Error | null;
  status: 'idle' | 'pending' | 'success' | 'error';
  dataUpdatedAt: number;
  errorUpdatedAt: number;
  fetchedAt: number;
  isLoading: boolean;
  isSuccess: boolean;
  isError: boolean;
  isIdle: boolean;
}

Example:

ts
const state = queryClient.getState<User>(['users', '1']);
if (state) {
  console.log('Status:', state.status);
  console.log('Is loading:', state.isLoading);
  console.log('Data:', state.data);
  console.log('Last updated:', new Date(state.dataUpdatedAt));
}

prefetchQuery<T>(options)

Prefetch a query to warm up the cache without consuming the result. Useful for loading data before it's needed. Errors are silently ignored.

Parameters:

  • options: QueryOptions<T> – Same as query() options

Returns: Promise<void>

Example:

ts
import { createQueryClient, createHttpClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const queryClient = createQueryClient();

// Prefetch user data when hovering over a link
onMouseEnter={() => {
  queryClient.prefetch({
    queryKey: ['users', userId],
    queryFn: () => http.get<User>(`/users/${userId}`),
  });
});

// Later when navigating, data is already cached
const user = await queryClient.fetch({
  queryKey: ['users', userId],
  queryFn: () => http.get<User>(`/users/${userId}`),
});

subscribe(queryKey, listener)

Subscribe to changes in a query's cache entry. The listener receives the full query state on every change.

Parameters:

  • queryKey: QueryKey – The query key to watch
  • listener: (state: QueryState<T>) => void – Callback function called with state on changes

State Object:

ts
{
  data: T | undefined;
  error: Error | null;
  status: 'idle' | 'pending' | 'success' | 'error';
  httpStatus?: number;
  httpOk?: boolean;
  isLoading: boolean;
  isSuccess: boolean;
  isError: boolean;
  isIdle: boolean;
  dataUpdatedAt: number;
  errorUpdatedAt: number;
  fetchedAt: number;
}

Returns: () => void – Unsubscribe function

Example:

ts
import { createQueryClient } from '@vielzeug/fetchit';

const queryClient = createQueryClient();

// Subscriber receives state automatically
const unsubscribe = queryClient.subscribe(['users', 1], (state) => {
  if (state.isLoading) {
    showSpinner();
  } else if (state.isSuccess) {
    console.log('User:', state.data);
    console.log('HTTP Status:', state.httpStatus);
  } else if (state.isError) {
    showError(state.error);
  }
});

// Later, unsubscribe
unsubscribe();

// React example
useEffect(() => {
  return queryClient.subscribe(['users', userId], (state) => {
    setUser(state.data);
    setLoading(state.isLoading);
  });
}, [userId]);
```### `subscribe<T>(queryKey, listener)`

Subscribe to query state changes. Returns an unsubscribe function.

**Parameters:**

- `queryKey: QueryKey` – The query key to watch
- `listener: (state: QueryState<T>) => void` – Callback invoked on state changes

**Returns:** `() => void` – Unsubscribe function

**Example:**

```ts
// Subscribe to changes
const unsubscribe = queryClient.subscribe(['users', userId], (state) => {
  console.log('User changed:', state.data);
  console.log('Loading:', state.isLoading);
  console.log('Error:', state.error);
});

// Clean up when done
unsubscribe();

React Hook Example:

tsx
useEffect(() => {
  return queryClient.subscribe(['users', userId], (state) => {
    setUser(state.data);
    setLoading(state.isLoading);
  });
}, [userId]);

clear()

Clears all cached queries and aborts in-flight requests.

Example:

ts
queryClient.clear();

prefetch<T>(options)

Prefetch a query without consuming the result. Useful for warming the cache.

Parameters:

  • options: QueryOptions<T> – Same as fetch() options

Returns: Promise<void>

Example:

ts
// Prefetch user data before navigation
await queryClient.prefetch({
  queryKey: ['users', '2'],
  queryFn: () => http.get('/users/2'),
});

Types

QueryKey

Array-based unique identifier for queries.

ts
type QueryKey = readonly unknown[];

Examples:

ts
['users'][('users', 1)][('posts', { status: 'published' })][('users', userId, 'posts')];

QueryOptions<T>

Configuration for query execution.

ts
type QueryOptions<T> = {
  queryKey: QueryKey;
  queryFn: () => Promise<T>;
  staleTime?: number;
  cacheTime?: number;
  enabled?: boolean;
  retry?: number | false;
  retryDelay?: number | ((attemptIndex: number) => number);
  refetchOnFocus?: boolean;
  refetchOnReconnect?: boolean;
  onSuccess?: (data: T) => void;
  onError?: (error: Error) => void;
};

MutationOptions<TData, TVariables>

Configuration for mutation execution.

ts
type MutationOptions<TData, TVariables> = {
  mutationFn: (variables: TVariables) => Promise<TData>;
  onSuccess?: (data: TData, variables: TVariables) => void;
  onError?: (error: Error, variables: TVariables) => void;
  onSettled?: (data: TData | undefined, error: Error | null, variables: TVariables) => void;
  retry?: number | false;
};

HttpClientOptions

Client initialization options.

ts
type HttpClientOptions = {
  baseUrl?: string;
  headers?: Record<string, string>;
  timeout?: number;
  defaultStaleTime?: number;
  defaultCacheTime?: number;
};

Note: Fetchit uses @vielzeug/toolkit's utilities:

HttpRequestConfig

Request configuration for REST methods.

ts
type HttpRequestConfig = Omit<RequestInit, 'body'> & {
  body?: unknown; // Auto-serialized to JSON or passed as-is for FormData/Blob
  params?: Record<string, string | number | boolean | undefined>; // Path parameters (replace :id or {id} in URL)
  query?: Record<string, string | number | boolean | undefined>; // Query string parameters (?key=value)
  dedupe?: boolean; // Enable/disable request deduplication for this request
};

Examples:

ts
// Query parameters
await http.get('/users', {
  query: { role: 'admin', active: true },
});
// → GET /users?role=admin&active=true

// Path parameters
await http.get('/users/:id', {
  params: { id: '123' },
});
// → GET /users/123

// Combined
await http.get('/users/:userId/posts', {
  params: { userId: '123' },
  query: { status: 'published', limit: 10 },
});
// → GET /users/123/posts?status=published&limit=10

RequestResponse<T>

Response object returned by all REST HTTP methods.

ts
type RequestResponse<T> = {
  data: T; // Parsed response data
  ok: boolean; // True if status is 2xx
  status: number; // HTTP status code
};

HttpError

Custom error class with request context.

ts
class HttpError extends Error {
  readonly url: string;
  readonly method: string;
  readonly status?: number;
  readonly originalError?: unknown;
}

Example:

ts
import { HttpError, createHttpClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });

try {
  await http.get('/users/1');
} catch (error) {
  if (error instanceof HttpError) {
    console.error(`${error.method} ${error.url} failed with status ${error.status}`);
  }
}

QueryStatus

Query state enumeration.

ts
type QueryStatus = 'idle' | 'pending' | 'success' | 'error';

Advanced Usage

Query Keys Best Practices

Use structured query keys for better cache management:

ts
// User details
['users', userId][
  // User details
  ('users', userId)
][
  // User posts
  ('users', userId, 'posts')
][
  // Filtered posts
  ('posts', { status: 'published', author: userId })
];

// Invalidate all user-related queries
queryClient.invalidate(['users']);

Optimistic Updates

ts
import { createHttpClient, createQueryClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const queryClient = createQueryClient();

// Optimistically update cache before mutation
queryClient.setData(['users', userId], (old) => ({
  ...old,
  name: 'New Name',
}));

// Perform mutation
await queryClient.mutate(
  {
    mutationFn: (data) => http.put<User>(`/users/${userId}`, { body: data }),
    onError: () => {
      // Revert on error
      queryClient.invalidate(['users', userId]);
    },
  },
  { name: 'New Name' },
);

Stale-While-Revalidate Pattern

ts
import { createHttpClient, createQueryClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const queryClient = createQueryClient();

// Return stale data immediately, fetch fresh data in background
const user = await queryClient.fetch({
  queryKey: ['users', userId],
  queryFn: () => http.get<User>(`/users/${userId}`),
  staleTime: 5000, // Data is fresh for 5 seconds
  gcTime: 300000, // Keep in cache for 5 minutes
});

Custom Retry Strategies

Fetchit uses @vielzeug/toolkit's retry() utility under the hood, giving you powerful retry strategies:

ts
import { createHttpClient, createQueryClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const queryClient = createQueryClient();

// Exponential backoff (default)
await queryClient.fetch({
  queryKey: ['users'],
  queryFn: () => http.get<User[]>('/users'),
  retry: 3, // Retry 3 times
  // Default: 1s, 2s, 4s, 8s... (max 30s)
});

// Custom retry delay function
await queryClient.fetch({
  queryKey: ['users'],
  queryFn: () => http.get<User[]>('/users'),
  retry: 3,
  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
});

// Fixed delay
await queryClient.fetch({
  queryKey: ['users'],
  queryFn: () => http.get<User[]>('/users'),
  retry: 5,
  retryDelay: 2000, // Wait 2s between retries
});

// Disable retries
await queryClient.fetch({
  queryKey: ['users'],
  queryFn: () => http.get<User[]>('/users'),
  retry: false, // No retries
});

How it works:

  • If retryDelay is a function, it's called with attemptIndex – 1 (0-based)
  • If retryDelay is a number, it's used as a fixed delay
  • If retryDelay is undefined, exponential backoff is used (1s → 2s → 4s → 8s, max 30s)
  • Retry requests respect the query's AbortController for cancellation

See retry() documentation for more details.

Request Deduplication

ts
import { createHttpClient, createQueryClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const queryClient = createQueryClient();

// Multiple identical queries = single network request
const [user1, user2, user3] = await Promise.all([
  queryClient.fetch({ queryKey: ['users', 1], queryFn: () => http.get<User>('/users/1') }),
  queryClient.fetch({ queryKey: ['users', 1], queryFn: () => http.get<User>('/users/1') }),
  queryClient.fetch({ queryKey: ['users', 1], queryFn: () => http.get<User>('/users/1') }),
]);
// Only one actual fetch is made, all receive the same result

FormData and File Uploads

ts
import { createHttpClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });

const formData = new FormData();
formData.append('file', fileInput.files[0]);
formData.append('name', 'document.pdf');

// Automatically detected and handled correctly
await http.post('/upload', { body: formData });

Binary Data

ts
import { createHttpClient } from '@vielzeug/fetchit';

const http = createHttpClient({ baseUrl: 'https://api.example.com' });

// Blob
const blob = new Blob(['content'], { type: 'text/plain' });
await http.post('/upload', { body: blob });

// ArrayBuffer
const buffer = new ArrayBuffer(8);
await http.post('/data', { body: buffer });

Reactive Data with Subscriptions

ts
import { createQueryClient } from '@vielzeug/fetchit';

const queryClient = createQueryClient();

// Subscribe to changes
const unsubscribe = queryClient.subscribe(['users', userId], () => {
  const user = queryClient.getData(['users', userId]);
  console.log('User updated:', user);
});

// Trigger update
queryClient.setData(['users', userId], newUserData);

// Clean up
unsubscribe();