Comparison
Use the Framework picker — the matrix below is core; stack-specific notes appear under each framework.
tanstack-fetch is an HTTP client shaped for TanStack Query. axios, ky, and ofetch can do the same network work — the difference is the call site.
Feature matrix
| Need | tanstack-fetch | axios | ky | ofetch |
|---|---|---|---|---|
Drop into TanStack Query queryFn | Returns data, throws FetchError, takes signal | Adapt | Adapt | Adapt |
| Typed error with status / body | FetchError + isFetchError | Partial | Partial | Partial |
| Handlers for 401–429 | First-class | Custom | Custom | Custom |
| Refresh token + single-flight | Interceptor + { action: 'retry' } | Custom | Custom | Custom |
| Next.js SSR cookies | ssr-forward plugin | Custom | Custom | Custom |
| SSE with Authorization | tanstack-fetch/sse | No | No | No |
| WebSocket / WebTransport with auth | api.ws / api.transport (/live) | No | No | No |
| NDJSON streaming (POST + headers) | api.stream (/live) | Custom | Custom | Custom |
| File upload + progress | api.upload() (/sse) | Yes | Limited | Limited |
| tRPC transport | tanstack-fetch/trpc | No | No | No |
| Bundle | 3.57KB gzip HTTP core | Larger | Small | Small |
Before / after
axios into Query
useQuery({
queryKey: ['users'],
queryFn: async ({ signal }) => {
const response = await axios.get('/users', { signal })
return response.data
},
})tanstack-fetch into Query
useQuery({
queryKey: ['users'],
queryFn: ({ signal }) => api.get<User[]>('/users', { signal }),
})No .data unwrap. HTTP failures throw, so Query’s isError path stays honest.
Migrating? CLI + mapping for axios, ky, ofetch, and raw fetch: Migrate.
Refresh token
import { createRefreshTokenInterceptor } from 'tanstack-fetch/plugins'
api.use(
'refresh-token',
createRefreshTokenInterceptor({
refresh: async () => {
const body = await api.post<{
accessToken: string
expiresIn: number
}>('/auth/refresh', {
interceptors: { eject: ['refresh-token', 'auth'] },
})
/* persist body.accessToken + expiresAt */
},
before: {
getExpiresAt: () => expiresAt,
skewMs: 60_000,
},
after: { enabled: true },
}),
)Full recipe: Refresh token.
SSR cookies
Next.js App Router fetch does not forward cookies automatically. ssr-forward plugin reads cookies() on the server and attaches them.
// server
import { cookies } from 'next/headers'
import { createSsrForwardInterceptor } from 'tanstack-fetch/plugins'
export const api = createFetch({
baseUrl: process.env.API_URL,
source: 'ssr',
interceptors: [createSsrForwardInterceptor()],
incoming: () => ({ cookie: (await cookies()).toString() }),
})
// client
const api = createFetch({ baseUrl: '/api', interceptors: [createSsrForwardInterceptor()] })Guide: SSR · Blog: Next.js fetch vs tanstack-fetch · Example: /examples/next-ssr.
SSE with Authorization
Browser EventSource cannot set Authorization. Use tanstack-fetch/sse (api.sse) instead.
import { createFetch } from 'tanstack-fetch/sse'
const api = createFetch({
baseUrl: import.meta.env.VITE_API_URL,
getToken: () => localStorage.getItem('access_token'),
})
api.sse<OrderEvent>('/orders/stream', {
onMessage: (data) => queryClient.setQueryData(['orders'], (prev) => [...prev, data]),
})- Full recipe: EventSource auth headers
- Reconnect with
Last-Event-ID:sse-resume - SSE guide · Blog posts for framework-specific SSE
WebSocket, WebTransport & NDJSON
axios, ky and ofetch stop at request / response. For sockets you'd usually add a second library and wire auth into it separately. tanstack-fetch/live reuses the same client:
import { createFetch } from 'tanstack-fetch/live'
import { createRefreshTokenInterceptor } from 'tanstack-fetch/plugins'
const api = createFetch({ baseUrl: '/api', getToken })
api.use('refresh-token', createRefreshTokenInterceptor({ refresh }))
const room = api.ws<ChatMessage, { id: string }>('/rooms/:id', { params: { id }, authIn: 'query' })
for await (const chunk of api.stream<Token>('/chat', { body: { prompt } })) render(chunk)- Fresh token on every reconnect, the same interceptors, and
refresh-tokenon 401 forapi.stream - WebSocket, WebTransport & streams guide · 1.8.0 release
When to keep axios
- You already have a large axios interceptor stack and no Query migration planned
- You need axios-specific adapters that are not fetch-based
Otherwise, if TanStack Query is the source of truth for loading and errors, tanstack-fetch matches that model with less glue.
React / Query
Use @tanstack/react-query + tanstack-fetch/react. Guide: React · TanStack Query.
Angular
Use @tanstack/angular-query-experimental + tanstack-fetch/angular. Guide: Angular. Deep dive: HttpClient vs tanstack-fetch.
Next.js
SSR cookies via ssr-forward. Guide: SSR · Next.js.
TanStack Start
Router context + Query. TanStack Query · tRPC recipe.
Remix
Loaders + optional Query hydration. tRPC · Remix.
