Skip to Content
✨ New: Plugin Support. Extend the core functionality with ease. Read more... 🎉
FAQ & Troubleshooting

FAQ & Troubleshooting

Common questions and solutions for endpoint-fetcher.

Common Questions

How do I handle query parameters?

Use a custom handler or path function:

// Option 1: Path function (simple) const searchUsers = get<{ query: string; page?: number }, User[]>( (input) => { const params = new URLSearchParams({ q: input.query }); if (input.page) params.set('page', input.page.toString()); return `/users/search?${params}`; } ); // Option 2: Custom handler (complex) const searchUsers = get<SearchParams, User[]>( '/users/search', async ({ input, fetch, baseUrl, path }) => { const params = new URLSearchParams(); params.set('q', input.query); if (input.filters) { Object.entries(input.filters).forEach(([key, value]) => { params.set(key, value.toString()); }); } const response = await fetch(`${baseUrl}${path}?${params}`, { method: 'GET', }); return response.json(); } );

Can I use endpoint-fetcher with React Query or SWR?

Yes! See the React Query and SWR integration examples.

Quick example:

import { useQuery } from '@tanstack/react-query'; function useUsers() { return useQuery({ queryKey: ['users'], queryFn: () => api.users.list(), }); }

How do I handle different base URLs for different endpoints?

Use groups with different configurations or modify URLs in hooks:

// Option 1: Multiple clients const apiV1 = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com/v1', }); const apiV2 = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com/v2', }); // Option 2: Modify in hooks const api = createApiClient({ legacy: group({ hooks: { beforeRequest: async (url, init) => { // Replace base URL for legacy endpoints const newUrl = url.replace('api.example.com', 'legacy-api.example.com'); return { url: newUrl, init }; }, }, endpoints: { /* ... */ }, }), }, { baseUrl: 'https://api.example.com', });

What’s the difference between hooks and custom handlers?

Hooks:

  • Apply to multiple endpoints (global, group, or endpoint level)
  • Modify requests/responses without replacing default behavior
  • Best for cross-cutting concerns (auth, logging, error tracking)

Custom Handlers:

  • Apply to a single endpoint
  • Replace the default request/response handling entirely
  • Best for special cases (file uploads, non-JSON responses, streaming)

Example:

// Use hooks for auth (applies to many endpoints) const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', hooks: { beforeRequest: async (url, init) => { // Add auth to ALL requests return { url, init: { ...init, headers: { ...init.headers, Authorization: token } } }; }, }, }); // Use custom handler for file upload (one special endpoint) const uploadFile = post<{ file: File }, { url: string }>( '/files', async ({ input, fetch, baseUrl, path }) => { const formData = new FormData(); formData.append('file', input.file); const response = await fetch(`${baseUrl}${path}`, { method: 'POST', body: formData, }); return response.json(); } );

How do I add authentication to all requests?

Use global hooks:

const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', hooks: { beforeRequest: async (url, init) => { const token = localStorage.getItem('jwt'); return { url, init: { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}`, } } }; }, }, });

How do I retry failed requests?

Use the afterResponse hook:

const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', hooks: { afterResponse: async (response, url, init) => { if (response.status >= 500) { const retryCount = parseInt((init.headers as any)['X-Retry-Count'] || '0'); if (retryCount < 3) { await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, retryCount))); return fetch(url, { ...init, headers: { ...init.headers, 'X-Retry-Count': (retryCount + 1).toString(), }, }); } } return response; }, }, });

Or create a retry plugin (see Plugins).

Can I use endpoint-fetcher in Node.js?

Yes! Just provide a fetch implementation:

import { createApiClient, group, get } from 'endpoint-fetcher'; import fetch from 'node-fetch'; const api = createApiClient({ users: group({ endpoints: { list: get<void, User[]>('/users'), } }), }, { baseUrl: 'https://api.example.com', fetch: fetch as any, });

How do I test my API client?

Mock the fetch instance:

const mockFetch: typeof fetch = async (input, init) => { const url = typeof input === 'string' ? input : input.toString(); if (url.includes('/users')) { return new Response( JSON.stringify([{ id: '1', name: 'Test User' }]), { status: 200, headers: { 'Content-Type': 'application/json' } } ); } return new Response('Not found', { status: 404 }); }; const api = createApiClient({ users: group({ endpoints: { list: get<void, User[]>('/users'), } }), }, { baseUrl: 'https://api.example.com', fetch: mockFetch, }); // Test const users = await api.users.list(); expect(users).toHaveLength(1);

How do I handle file uploads?

Use a custom handler with FormData:

const api = createApiClient({ files: group({ endpoints: { upload: post<{ file: File; name: string }, { url: string }>( '/files', async ({ input, fetch, baseUrl, path }) => { const formData = new FormData(); formData.append('file', input.file); formData.append('name', input.name); const response = await fetch(`${baseUrl}${path}`, { method: 'POST', body: formData, // Don't set Content-Type - browser sets it with boundary }); return response.json(); } ), } }), }, { baseUrl: 'https://api.example.com', }); // Usage const file = new File(['content'], 'document.pdf'); await api.files.upload({ file, name: 'My Document' });

How do I cancel requests?

Use AbortController:

const controller = new AbortController(); try { const users = await api.users.list(); } catch (error) { if (error.name === 'AbortError') { console.log('Request cancelled'); } } // Cancel after 5 seconds setTimeout(() => controller.abort(), 5000);

Add abort signal in hooks:

const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', hooks: { beforeRequest: async (url, init) => { return { url, init: { ...init, signal: yourAbortController.signal, } }; }, }, });

Troubleshooting

TypeScript Errors

Error: “Type ‘X’ is not assignable to type ‘Y’”

Problem: Type mismatch between defined types and actual data.

Solution: Ensure your type definitions match the API response:

// ❌ Wrong - API returns snake_case type User = { id: string; firstName: string; }; // ✅ Correct - Match API response type User = { id: string; first_name: string; }; // Or transform in custom handler const getUser = get<{ id: string }, User>( (input) => `/users/${input.id}`, async ({ fetch, baseUrl, path }) => { const response = await fetch(`${baseUrl}${path}`); const raw = await response.json(); return { id: raw.id, firstName: raw.first_name, // Transform here }; } );

Error: “Property does not exist on type”

Problem: Accessing properties not defined in your types.

Solution: Add missing properties to type definitions:

// ❌ Wrong type User = { id: string; name: string; }; const user = await api.users.getById({ id: '123' }); console.log(user.email); // Error: Property 'email' does not exist // ✅ Correct type User = { id: string; name: string; email: string; // Add missing property };

Runtime Errors

Error: “Failed to fetch” or “Network request failed”

Problem: Network connectivity issues or CORS errors.

Solutions:

  1. Check CORS configuration on your API server
  2. Verify the base URL is correct
  3. Check network connection
  4. Add error handling:
const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', hooks: { onError: async (error) => { if (error instanceof TypeError && error.message.includes('fetch')) { console.error('Network error - check your connection'); } }, }, });

Error: “Unexpected token < in JSON at position 0”

Problem: API returned HTML instead of JSON (often a 404 or 500 page).

Solution: Check response status before parsing JSON:

const endpoint = get<void, User[]>( '/users', async ({ fetch, baseUrl, path }) => { const response = await fetch(`${baseUrl}${path}`); if (!response.ok) { console.error('Response status:', response.status); throw new Error(`HTTP ${response.status}`); } return response.json(); } );

Error: Infinite loop or maximum call stack exceeded

Problem: Hook is calling itself recursively.

Solution: Add guards to prevent infinite recursion:

// ❌ Wrong - infinite loop const api = createApiClient({ auth: group({ endpoints: { refresh: post<void, { token: string }>('/auth/refresh'), } }), }, { baseUrl: 'https://api.example.com', hooks: { afterResponse: async (response, url) => { if (response.status === 401) { const { token } = await api.auth.refresh(); // ❌ Calls itself! // ... } return response; }, }, }); // ✅ Correct - exclude refresh endpoint const api = createApiClient({ auth: group({ endpoints: { refresh: post<void, { token: string }>('/auth/refresh'), } }), }, { baseUrl: 'https://api.example.com', hooks: { afterResponse: async (response, url) => { if (response.status === 401 && !url.includes('/auth/refresh')) { const { token } = await api.auth.refresh(); // ✅ Safe // ... } return response; }, }, });

Type Safety Issues

Error types not working in catch blocks

Problem: TypeScript doesn’t enforce types in catch blocks.

Solution: Use type assertions:

type ApiError = { message: string; code: string }; try { await api.users.create({ name: 'John' }); } catch (err: unknown) { // Type assertion needed const error = err as { status: number; error: ApiError }; console.log(error.error.code); // Now type-safe }

Autocomplete not working

Problem: TypeScript can’t infer types.

Solutions:

  1. Ensure types are defined:
// ❌ Missing types const getUser = get('/users'); // ✅ With types const getUser = get<{ id: string }, User>((input) => `/users/${input.id}`);
  1. Use as const for literal types:
const ROLES = ['admin', 'user'] as const; type Role = typeof ROLES[number];

Plugin Issues

Plugins not executing

Problem: Plugin not added to plugins array.

Solution:

// ❌ Wrong - plugin defined but not added const myPlugin = createPlugin(() => ({ hooks: { /* ... */ } })); const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', // Missing plugins array }); // ✅ Correct - plugin added const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', plugins: [myPlugin()], // Add plugin });

Plugin hooks not firing

Problem: Plugin hooks execute before global hooks.

Solution: Check execution order - plugins → global → group → endpoint.

Performance Issues

Requests are slow

Solutions:

  1. Check network conditions
  2. Add caching:
import { cache } from '@endpoint-fetcher/cache'; const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', plugins: [cache({ ttl: 300 })], });
  1. Monitor with performance hooks:
const api = createApiClient({ users: group({ endpoints: { /* ... */ } }), }, { baseUrl: 'https://api.example.com', hooks: { beforeRequest: async (url, init) => { console.time(url); return { url, init }; }, afterResponse: async (response, url) => { console.timeEnd(url); return response; }, }, });

Migration Guides

From Axios

Axios:

const response = await axios.get('/users', { baseURL: 'https://api.example.com', headers: { Authorization: `Bearer ${token}` }, }); const users = response.data;

endpoint-fetcher:

const api = createApiClient({ users: group({ endpoints: { list: get<void, User[]>('/users'), } }), }, { baseUrl: 'https://api.example.com', hooks: { beforeRequest: async (url, init) => ({ url, init: { ...init, headers: { ...init.headers, Authorization: `Bearer ${token}`, } } }), }, }); const users = await api.users.list();

From Native Fetch

Fetch:

const response = await fetch('https://api.example.com/users', { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, }); const users = await response.json();

endpoint-fetcher:

const api = createApiClient({ users: group({ endpoints: { list: get<void, User[]>('/users'), } }), }, { baseUrl: 'https://api.example.com', defaultHeaders: { 'Authorization': `Bearer ${token}`, }, }); const users = await api.users.list();

Still Need Help?

  • Check Examples for real-world patterns
  • Review API Reference for detailed documentation
  • Search existing issues on GitHub 
  • Open a new issue with a minimal reproduction