Fetchit API Reference
Complete API documentation for @vielzeug/fetchit v2.
Overview
Fetchit provides two separate clients for maximum flexibility:
- HTTP Client (
createHttpClient) - Pure HTTP operations without caching - 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?: HttpClientOptionsbaseUrl?: string- Base URL for all requestsheaders?: Record<string, string>- Default headers for all requeststimeout?: 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:
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 requestpost<T>(url, config?): Promise<T>- POST requestput<T>(url, config?): Promise<T>- PUT requestpatch<T>(url, config?): Promise<T>- PATCH requestdelete<T>(url, config?): Promise<T>- DELETE requestrequest<T>(method, url, config?): Promise<T>- Custom HTTP methodsetHeaders(headers): void- Update global headersgetHeaders(): Record<string, string>- Get current headers
createQueryClient(options?)
Creates a pure query management client. Works with any HTTP client or fetch function.
Parameters:
options?: QueryClientOptionsstaleTime?: number- Default stale time in ms (default: 0)gcTime?: number- Default garbage collection time in ms (default: 300000)cache?: object- Nested cache configuration (alternative)staleTime?: number- Time before data is stale (default: 0)gcTime?: number- Garbage collection time (default: 300000)
refetch?: object- Refetch configurationonFocus?: boolean- Refetch on window focus (default: false)onReconnect?: boolean- Refetch on reconnect (default: false)
Note: Options can be flat (
staleTime,gcTime) or nested (cache.staleTime,cache.gcTime). Both formats are supported.
Returns: Query client instance
Example:
import { createQueryClient, createHttpClient } from '@vielzeug/fetchit';
// Flat options (recommended)
const queryClient = createQueryClient({
staleTime: 5000,
gcTime: 300000,
});
// Nested options (also supported)
const queryClient = createQueryClient({
cache: { 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 cachingmutate<TData, TVariables>(options, variables): Promise<TData>- Execute a mutationprefetch<T>(options): Promise<void>- Prefetch a querygetData<T>(queryKey): T | undefined- Get cached datasetData<T>(queryKey, data | updater): void- Set/update cached datagetState<T>(queryKey): QueryState<T> | null- Get full query stateinvalidate(queryKey): void- Invalidate query (supports pattern matching)subscribe<T>(queryKey, listener): () => void- Subscribe to query changesunsubscribe<T>(queryKey, listener): void- Unsubscribe from changesclearCache(): void- Clear all cached datagetCacheSize(): number- Get number of cached queries
Method Aliases (TanStack Query compatibility):
fetchQuery<T>(options): Promise<T>- Alias forfetch()getQueryData<T>(queryKey): T | undefined- Alias forgetData()setQueryData<T>(queryKey, data): void- Alias forsetData()getQueryState<T>(queryKey): QueryState<T> | null- Alias forgetState()invalidateQueries(queryKey): void- Alias forinvalidate()prefetchQuery<T>(options): Promise<void>- Alias forprefetch()
Type-Safe Query Keys
While there's no built-in helper, you can create type-safe query keys using TypeScript's as const:
Example:
// 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());
// Typo caught at compile time
queryClient.invalidateQueries(queryKeys.user.detail('123')); // ❌ Error!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 datastaleTime?: 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 retriesonSuccess?: (data: T) => void- Success callbackonError?: (error: Error) => void- Error callback
Returns: Promise<T> - The fetched data
Example:
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 executeretry?: number | false- Number of retry attempts (default: false)retryDelay?: number | ((attempt: number) => number)- Delay between retriesonSuccess?: (data: TData, variables: TVariables) => void- Success callbackonError?: (error: Error, variables: TVariables) => void- Error callbackonSettled?: (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:
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 parametersdedupe?: boolean- Enable/disable deduplication (default: true)signal?: AbortSignal- AbortSignal for request cancellation
Example:
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:
const user = await http.post<User>('/users', {
body: { name: 'Alice', email: 'alice@example.com' },
});
console.log(user.id); // Direct accessput<T>(url, config?)
Makes a PUT request and returns the data directly.
Example:
const user = await http.put<User>('/users/1', {
body: { name: 'Alice Smith' },
});
console.log(user); // Updated userpatch<T>(url, config?)
Makes a PATCH request.
Example:
const user = await http.patch<User>('/users/1', {
body: { email: 'newemail@example.com' },
});delete<T>(url, config?)
Makes a DELETE request.
Example:
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 URLconfig?: HttpRequestConfig- Request configuration
Returns: Promise<T>
Example:
// 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:
// Set auth token
http.setHeaders({ Authorization: `Bearer ${token}` });
// Remove auth token
http.setHeaders({ Authorization: undefined });getHeaders()
Get current global headers.
Returns: Record<string, string>
Example:
const headers = http.getHeaders();
console.log(headers.Authorization);Query Client Cache Management
invalidate(queryKey) / invalidateQueries(queryKey)
Invalidates and removes queries from cache. Supports pattern matching!
Note:
invalidateQueriesis an alias forinvalidate(TanStack Query compatibility).
Pattern Matching:
- Exact match:
['users', '1']matches only['users', '1'] - Prefix match:
['users']matches['users'],['users', '1'],['users', 'list', {...}], etc.
Example:
// Invalidate specific user
queryClient.invalidate(['users', '1']);
// Invalidate ALL user queries (pattern 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 keydata: T | ((old: T | undefined) => T)- New data or updater function
Example:
// 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:
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:
{
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:
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 asquery()options
Returns: Promise<void>
Example:
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 watchlistener: (state: QueryState<T>) => void- Callback function called with state on changes
State Object:
{
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:
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:
useEffect(() => {
return queryClient.subscribe(['users', userId], (state) => {
setUser(state.data);
setLoading(state.isLoading);
});
}, [userId]);unsubscribe<T>(queryKey, listener)
Manually unsubscribe a listener (alternative to using the returned function).
Parameters:
queryKey: QueryKey- The query keylistener: (state: QueryState<T>) => void- The listener to remove
Example:
const listener = (state) => console.log('Changed:', state);
queryClient.subscribe(['users', '1'], listener);
queryClient.unsubscribe(['users', '1'], listener);clearCache()
Clears all cached queries and aborts in-flight requests.
Example:
queryClient.clearCache();getCacheSize()
Returns the number of cached queries.
Returns: number
Example:
const size = queryClient.getCacheSize();
console.log(`Cache has ${size} queries`);prefetch<T>(options)
Prefetch a query without consuming the result. Useful for warming the cache.
Parameters:
options: QueryOptions<T>- Same asfetch()options
Returns: Promise<void>
Example:
// Prefetch user data before navigation
await queryClient.prefetch({
queryKey: ['users', '2'],
queryFn: () => http.get('/users/2'),
});Types
QueryKey
Array-based unique identifier for queries.
type QueryKey = readonly unknown[];Examples:
['users'][('users', 1)][('posts', { status: 'published' })][('users', userId, 'posts')];QueryOptions<T>
Configuration for query execution.
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.
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.
type HttpClientOptions = {
baseUrl?: string;
headers?: Record<string, string>;
timeout?: number;
defaultStaleTime?: number;
defaultCacheTime?: number;
};Note: Fetchit uses @vielzeug/toolkit's utilities:
- Cache Management: Powered by cache()
- Retry Logic: Powered by retry()
- Logging: Powered by @vielzeug/logit
HttpRequestConfig
Request configuration for REST methods.
type HttpRequestConfig = Omit<RequestInit, 'body'> & {
body?: unknown; // Auto-serialized to JSON or passed as-is for FormData/Blob
params?: Record<string, string | number | undefined>; // Query parameters
};RequestResponse<T>
Response object returned by all REST HTTP methods.
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.
class HttpError extends Error {
readonly url: string;
readonly method: string;
readonly status?: number;
readonly originalError?: unknown;
}Example:
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.
type QueryStatus = 'idle' | 'pending' | 'success' | 'error';Advanced Usage
Query Keys Best Practices
Use structured query keys for better cache management:
// 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
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
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:
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
retryDelayis a function, it's called withattemptIndex - 1(0-based) - If
retryDelayis a number, it's used as a fixed delay - If
retryDelayis undefined, exponential backoff is used (1s → 2s → 4s → 8s, max 30s) - Retry requests respect the query's
AbortControllerfor cancellation
See retry() documentation for more details.
Request Deduplication
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 resultFormData and File Uploads
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
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
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();