TanStack Query doesn't care how you fetch. It only asks one thing of your queryFn: return data, throw on failure, and respect the AbortSignal.
Most HTTP clients don't do that out of the box, so every project grows the same glue.
The usual glue
queryFn: async ({ signal }) => { try { const res = await axios.get('/users', { signal }) return res.data } catch (e) { if (axios.isAxiosError(e) && e.response?.status === 401) { await refreshToken() // what if 5 queries hit 401 at once? // retry...? } throw normalizeError(e) } } Multiply that by every endpoint, add SSR cookie forwarding and an SSE stream that needs an Authorization header, and you have a small framework nobody meant to write.
The same thing with tanstack-fetch
import { createFetch } from 'tanstack-fetch' import { createAuthInterceptor, createStatusInterceptor } from 'tanstack-fetch/plugins' export const api = createFetch({ baseUrl: 'https://api.example.com', interceptors: [ createAuthInterceptor({ getToken: () => localStorage.getItem('access_token') }), createStatusInterceptor({ 401: () => logout() }), ], }) useQuery({ queryKey: ['users'], queryFn: ({ signal }) => api.get<User[]>('/users', { signal }), }) - Returns the parsed body directly
- Throws
FetchError, andisFetchError(e)gives youstatusandbody, fully typed - Passes
signalthrough, so Query cancellation just works
Typed errors in the UI
const { error } = useQuery(...) if (isFetchError(error) && error.status === 404) return <NotFound /> Only pay for what you use
| Import | What | gzip |
|---|---|---|
tanstack-fetch | slim HTTP core | 3.57KB |
tanstack-fetch/plugins | interceptors | 1.13KB |
tanstack-fetch/sse | + SSE, upload, progress | 6.33KB |
tanstack-fetch/react | provider + hooks | 0.99KB |
There are also entry points for Vue/Nuxt, Solid, Svelte, Angular, tRPC and a DevTools dock.
SSE with auth
The browser's EventSource can't send headers. tanstack-fetch/sse can:
import { createFetch } from 'tanstack-fetch/sse' const api = createFetch({ baseUrl: 'https://api.example.com', getToken: () => localStorage.getItem('access_token'), }) const stream = api.sse<OrderEvent>('/orders/stream', { onMessage: (data) => console.log(data), }) stream.close() Migrating
npx tanstack-fetch doctor It scans your project and scaffolds a migration from axios, ky or ofetch.
Links
- Docs & playground: https://mohamadgarmabi.github.io/tanstack-fetch/
- GitHub: https://github.com/mohamadgarmabi/tanstack-fetch
- npm: https://www.npmjs.com/package/tanstack-fetch
Not an official TanStack package. It's built for the same mental model. I'd love feedback in the comments, especially on where the core/plugin line should be.
