Skip to content
VersionSizeTypeScriptDependencies
Fetchit Logo

Fetchit

Fetchit is a modern, type-safe HTTP client with intelligent caching and query management for browser and Node.js. Inspired by TanStack Query but significantly simpler, it provides separate HTTP and Query clients for maximum flexibility.

What Problem Does Fetchit Solve?

Modern applications need more than just HTTP requests - they need intelligent caching, request deduplication, optimistic updates, and retry logic. Fetchit provides all of this out of the box with a clean, type-safe API.

Traditional Approach:

ts
// Manual caching, deduplication, and error handling
const cache = new Map();
const pending = new Map();

async function fetchUser(id: number) {
  // Check cache
  if (cache.has(id)) return cache.get(id);

  // Deduplicate requests
  if (pending.has(id)) return pending.get(id);

  // Make request
  const promise = fetch(`/users/${id}`).then((r) => r.json());
  pending.set(id, promise);

  try {
    const data = await promise;
    cache.set(id, data);
    return data;
  } finally {
    pending.delete(id);
  }
}

With Fetchit - Use HTTP client for simple requests:

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

const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const user = await http.get('/users/1');

Or use Query client for advanced caching:

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, // Fresh for 5 seconds
});

Comparison with Alternatives

FeatureFetchitTanStack QueryAxiosNative Fetch
TypeScript Support✅ First-class✅ First-class✅ Good⚠️ Basic
Request Deduplication✅ Built-in✅ Built-in
Smart Caching✅ Built-in✅ Built-in⚠️ Via plugins
Pattern Invalidation✅ Built-in✅ Built-in
Auto JSON Parsing✅ Yes❌ Manual✅ Yes⚠️ Manual
Timeout Support✅ Built-in✅ Built-in⚠️ AbortController
Bundle Size (gzip)2.9 KB~13 KB~13 KB0 KB
Node.js Support✅ Yes✅ Yes✅ Yes✅ (v18+)
Dependencies207+0
Request Retry✅ Built-in✅ Built-in⚠️ Via plugins
React Hooks✅ Yes
Framework Agnostic✅ Yes✅ Yes✅ Yes✅ Yes

When to Use Fetchit

✅ Use Fetchit when you:

  • Need smart caching without the complexity of TanStack Query
  • Want a lightweight alternative to TanStack Query (~3.2 KB vs ~13 KB)
  • Build TypeScript applications requiring full type safety
  • Need automatic request deduplication to prevent redundant calls
  • Want built-in caching and retry logic out of the box
  • Prefer a framework-agnostic solution (no React dependency)
  • Need both simple HTTP requests AND query management in one package

❌ Consider alternatives when you:

  • Need React hooks integration (use TanStack Query directly)
  • Already heavily invested in Axios ecosystem
  • Need extremely minimal bundle size (use native fetch)
  • Building simple scripts with few HTTP requests
  • Need GraphQL-specific features

🚀 Key Features

  • Separate Clients: HTTP client and Query client work independently
  • Type-Safe: Full TypeScript support with generic types and inference
  • Smart Caching: Built-in caching with configurable staleness and GC
  • Deduplication: Automatically prevent concurrent identical requests
  • Pattern Invalidation: invalidate(['users']) matches all user-related queries
  • Auto Parsing: Intelligent handling of JSON, text, and binary data
  • Request Retry: Automatic retry powered by @vielzeug/toolkit's retry utility
  • Observable State: Subscribe to query changes for real-time updates
  • Method Aliases: TanStack Query-compatible naming for familiarity
  • Rich Error Context: Custom HttpError class with URL, method, and status
  • Minimal Dependencies: Only @vielzeug/toolkit and @vielzeug/logit

🏁 Quick Start

Installation

sh
pnpm add @vielzeug/fetchit
sh
npm install @vielzeug/fetchit
sh
yarn add @vielzeug/fetchit

Basic Usage

Option 1: HTTP Client Only (Simple)

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

// 1. Create HTTP client
const http = createHttpClient({
  baseUrl: 'https://api.example.com',
  timeout: 5000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// 2. Make type-safe requests
interface User {
  id: string;
  name: string;
  email: string;
}

// GET request - returns raw data
const user = await http.get<User>('/users/1');
console.log(user.name); // Type-safe!

// POST request with body
const created = await http.post<User>('/users', {
  body: {
    name: 'Alice',
    email: 'alice@example.com',
  },
});

// PUT request
const updated = await http.put<User>('/users/1', {
  body: { name: 'Alice Smith' },
});

// DELETE request
await http.delete('/users/1');

Option 2: Query Client for Advanced Caching

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

// 1. Create clients
const http = createHttpClient({ baseUrl: 'https://api.example.com' });
const queryClient = createQueryClient({
  cache: {
    staleTime: 5000,
    gcTime: 300000,
  },
});

// 2. Define type-safe 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,
  },
} as const;

// 3. Fetch with caching
const user = await queryClient.fetch({
  queryKey: queryKeys.users.detail('1'),
  queryFn: () => http.get<User>('/users/1'),
  staleTime: 5000,
});

// 4. Mutations
await queryClient.mutate(
  {
    mutationFn: (newUser: Partial<User>) => http.post('/users', { body: newUser }),
    onSuccess: (data) => {
      // Invalidate and refetch
      queryClient.invalidate(queryKeys.users.all());
    },
  },
  { name: 'Alice', email: 'alice@example.com' },
);

Option 3: Use Any Fetch Function with Query Client

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

const queryClient = createQueryClient();

// Use with native fetch
const data = await queryClient.fetch({
  queryKey: ['todos', '1'],
  queryFn: () => fetch('https://api.example.com/todos/1').then((r) => r.json()),
});

// Use with axios
import axios from 'axios';

const user = await queryClient.fetch({
  queryKey: ['users', '1'],
  queryFn: () => axios.get('/users/1').then((r) => r.data),
});

Real-World Example: API Client with Auth

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

// Create HTTP client
const http = createHttpClient({
  baseUrl: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
  },
});

// Create query client for caching
const queryClient = createQueryClient({
  cache: { staleTime: 5000, gcTime: 300000 },
});

// Define type-safe query keys manually
export const queryKeys = {
  users: {
    all: () => ['users'] as const,
    detail: (id: string) => ['users', id] as const,
  },
  profile: () => ['profile'] as const,
} as const;

// Update auth token dynamically
export function setAuthToken(token: string) {
  http.setHeaders({
    Authorization: `Bearer ${token}`,
  });
}

// Remove auth token (e.g., on logout)
export function clearAuth() {
  http.setHeaders({
    Authorization: undefined, // Removes the header
  });
  queryClient.clearCache(); // Clear cached authenticated requests
}

// Wrapper with error handling
async function apiRequest<T>(
  method: 'get' | 'post' | 'put' | 'delete',
  url: string,
  options?: { body?: unknown },
): Promise<T> {
  try {
    return await http[method]<T>(url, options);
  } catch (error) {
    if (error instanceof HttpError) {
      if (error.status === 401) {
        // Handle unauthorized - redirect to login
        window.location.href = '/login';
      }
      throw new Error(`Request failed: ${error.message}`);
    }
    throw error;
  }
}

// Use throughout your app with query caching
export const fetchUser = (id: string) =>
  queryClient.fetch({
    queryKey: queryKeys.users.detail(id),
    queryFn: () => apiRequest<User>('get', `/users/${id}`),
  });

export const updateProfile = (data: Partial<User>) =>
  queryClient.mutate(
    {
      mutationFn: (vars: Partial<User>) => apiRequest<User>('put', '/profile', { body: vars }),
      onSuccess: () => queryClient.invalidate(queryKeys.profile()),
    },
    data,
  );

Framework Integration: React

tsx
import { createHttpClient, createQueryClient } from '@vielzeug/fetchit';
import { useEffect, useState } from 'react';

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

const queryClient = createQueryClient();

function UserProfile({ userId }: { userId: string }) {
  const [state, setState] = useState({
    data: null as User | null,
    isLoading: true,
    error: null as Error | null,
  });

  useEffect(() => {
    // Subscribe to query state changes
    const unsubscribe = queryClient.subscribe(['users', userId], (newState) => {
      setState({
        data: newState.data,
        isLoading: newState.isLoading,
        error: newState.error,
      });
    });

    // Fetch data
    queryClient
      .fetch({
        queryKey: ['users', userId],
        queryFn: () => http.get<User>(`/users/${userId}`),
        staleTime: 5000,
      })
      .catch(() => {
        // Error handled by subscription
      });

    return () => {
      unsubscribe();
    };
  }, [userId]);

  if (state.isLoading) return <div>Loading...</div>;
  if (state.error) return <div>Error: {state.error.message}</div>;
  if (!state.data) return <div>User not found</div>;

  return (
    <div>
      <h1>{state.data.name}</h1>
      <p>{state.data.email}</p>
    </div>
  );
}

📚 Documentation

  • Usage Guide: Service configuration, interceptors, and error handling
  • API Reference: Complete documentation of all methods and options
  • Examples: Patterns for caching, cancellation, and file uploads

❓ FAQ

How is Fetchit different from TanStack Query?

Fetchit is inspired by TanStack Query but significantly simpler and lighter:

  • Smaller bundle: ~3.2 KB vs ~13 KB (gzipped)
  • No React dependency: Works with any framework (Vue, Svelte, vanilla JS)
  • Simpler API: Fewer concepts, easier to learn
  • Built-in HTTP client: No need for separate fetch library
  • No hooks: Use with any framework or vanilla JS
  • Pattern invalidation: Built-in prefix matching for cache invalidation

Use TanStack Query if you need React hooks integration. Use Fetchit for a simpler, framework-agnostic solution.

How is Fetchit different from Axios?

Fetchit is TypeScript-first with modern caching built-in:

  • TypeScript: First-class TypeScript support with full type inference
  • Smart caching: Built-in query caching and deduplication
  • Smaller bundle: ~3.2 KB vs ~13 KB (gzipped)
  • Modern: Uses native fetch API under the hood
  • Pattern invalidation: Powerful cache management

Does Fetchit work in Node.js?

Yes! Fetchit works in both browser and Node.js (v18+ recommended for native fetch support).

Can I use Fetchit with React Query or SWR?

Absolutely! Fetchit works great as the data fetching layer for these libraries.

How do I handle file uploads?

Fetchit automatically detects FormData and handles it correctly:

ts
const formData = new FormData();
formData.append('file', file);

// Content-Type is set automatically by the browser
await http.post('/upload', { body: formData });

Is request deduplication automatic?

Yes! Concurrent identical requests are automatically deduplicated to prevent redundant network calls.

How do I manage the cache?

Use the Query Client's built-in cache management methods powered by @vielzeug/toolkit's cache():

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

const queryClient = createQueryClient();

// Invalidate specific query (removes from cache and aborts in-flight requests)
queryClient.invalidate(['users', userId]);

// Manually set cache data
queryClient.setData(['users', 1], { id: 1, name: 'Alice' });

// Get cached data
const user = queryClient.getData(['users', 1]);

// Get query state
const state = queryClient.getState(['users', 1]);
console.log(state.status, state.data, state.error);

// Clear all cache
queryClient.clearCache();

// Get cache size
const size = queryClient.getCacheSize();

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

How do I handle authentication tokens?

Use the HTTP client's setHeaders to update auth headers dynamically:

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

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

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

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

// Get current headers
const headers = http.getHeaders();

🐛 Troubleshooting

CORS errors

Problem

Cross-origin requests blocked.

Solution

Ensure your server has proper CORS headers:

ts
// Server-side (Express example)
app.use(
  cors({
    origin: 'https://your-domain.com',
    credentials: true,
  }),
);

TypeScript type inference not working

Problem

Response data type not inferred.

Solution

Explicitly specify response type:

ts
// ✅ Correct - returns data directly
const user = await http.get<User>('/users/1');
console.log(user.name); // Type-safe

// ❌ Type is 'unknown'
const user = await http.get('/users/1');

Request cancelled errors

Problem: Getting abort/cancellation errors.

Solution: Handle cancellation properly:

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

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

Cache not working as expected

Problem: Getting stale data or cache not invalidating.

Solution: Use Query Client cache management methods:

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

const queryClient = createQueryClient();

// Invalidate specific query to force refetch
queryClient.invalidate(['users', userId]);

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

// Clear all cache
queryClient.clearCache();

// Manually update cache
queryClient.setData(['users', userId], updatedData);

🤝 Contributing

Found a bug or want to contribute? Check our GitHub repository.

📄 License

MIT © Helmuth Duarte


Tip: Fetchit is part of the Vielzeug ecosystem, which includes utilities for storage, logging, permissions, and more.