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:
- Check CORS configuration on your API server
- Verify the base URL is correct
- Check network connection
- 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:
- Ensure types are defined:
// ❌ Missing types
const getUser = get('/users');
// ✅ With types
const getUser = get<{ id: string }, User>((input) => `/users/${input.id}`);- Use
as constfor 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:
- Check network conditions
- Add caching:
import { cache } from '@endpoint-fetcher/cache';
const api = createApiClient({
users: group({ endpoints: { /* ... */ } }),
}, {
baseUrl: 'https://api.example.com',
plugins: [cache({ ttl: 300 })],
});- 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