Skip to content

Configuration ​

Two paths: simple (most apps) and advanced (plugins + interceptors).

Live demo · real createFetch

Trigger HTTP statuses

Mock fetch returns the code you click. Handlers fire before FetchError is thrown — including 429 with Retry-After.

Tap a status — real createFetch + mock fetch

Event log

Handlers and throws show up here.

const api = createFetch({
  interceptors: [
    createStatusInterceptor({
      401: () => logout(),
      429: ({ context }) => {
        const waitMs = parseRetryAfter(context.response?.headers)
        // show toast / backoff…
      },
      '4xx': ({ status }) => console.warn('4xx', status),
    }),
  ],
})

Simple path ​

Set API URL, attach a token, decide what happens on 4xx / 5xx — including 429.

ts
import { createFetch, parseRetryAfter } from 'tanstack-fetch'
import { createAuthInterceptor, createStatusInterceptor } from 'tanstack-fetch/plugins'

export const api = createFetch({
  baseUrl: import.meta.env.VITE_API_URL,
  interceptors: [
    createAuthInterceptor({ getToken: () => localStorage.getItem('access_token') }),
    createStatusInterceptor({
      401: () => {
        localStorage.removeItem('access_token')
        window.location.href = '/login'
      },
      403: () => {
        console.warn('No permission')
      },
      404: ({ error }) => {
        console.warn('Missing resource', error.message)
      },
      429: ({ context }) => {
        const waitMs = parseRetryAfter(context.response?.headers, 1000)
        console.warn('Rate limited — retry in', waitMs, 'ms')
      },
      '4xx': ({ status }) => {
        console.warn('Other 4xx', status) // any 4xx without its own handler
      },
      '5xx': ({ status }) => {
        console.error('Server error', status) // 500–599
      },
    }),
  ],
})

Status shortcuts ​

Full entries only

These options are accepted by createFetch from tanstack-fetch/sse / /live, by createTRPCFetch (/trpc) and by the framework providers (FetchProvider, createFetchPlugin, …). The slim core tanstack-fetch takes the interceptors above instead: getToken / auth → createAuthInterceptor, onX / onStatus → createStatusInterceptor({ 401, 403, …, '4xx', '5xx' }).

ts
import { createFetch } from 'tanstack-fetch/sse'

export const api = createFetch({
  baseUrl: import.meta.env.VITE_API_URL,
  getToken: () => localStorage.getItem('access_token'),
  onUnauthorized: () => {
    window.location.href = '/login'
  },
  onServerError: ({ status }) => console.error('Server error', status),
})
OptionWhen
getToken / authEvery request — Bearer token
onBadRequestHTTP 400
onUnauthorizedHTTP 401
onForbiddenHTTP 403
onNotFoundHTTP 404
onMethodNotAllowedHTTP 405
onRequestTimeoutHTTP 408
onConflictHTTP 409
onGoneHTTP 410
onPayloadTooLargeHTTP 413
onUnsupportedMediaTypeHTTP 415
onUnprocessableEntityHTTP 422
onTooManyRequestsHTTP 429
onUnavailableForLegalReasonsHTTP 451
onClientErrorAny other 4xx (4xx)
onServerErrorHTTP 5xx
onStatusAdvanced map (exact / buckets / default)

Resolution order: exact status → 5xx / 4xx buckets → default.

Advanced auth + status map ​

ts
createFetch({
  baseUrl: 'https://api.example.com',
  interceptors: [
    createAuthInterceptor({
      getToken: async () => (await cookies()).get('token')?.value,
      header: 'authorization',
      scheme: 'Bearer', // use '' for a raw token / API key
    }),
    createStatusInterceptor({
      401: () => redirect('/login'),
      403: () => toast.error('Forbidden'),
      404: () => toast.error('Not found'),
      429: ({ context }) => {
        const delayMs = parseRetryAfter(context.response?.headers, 1000)
        return { action: 'retry', delayMs }
      },
      500: () => toast.error('Server error'),
      '4xx': ({ status }) => console.warn('client', status),
      '5xx': ({ status }) => console.error('upstream', status),
      default: ({ status }) => console.warn('unhandled', status),
    }),
  ],
})

On the full entries / providers the same objects go in auth: { … } and onStatus: { … }.

Advanced path — plugins + interceptors ​

ts
import { createFetch, parseRetryAfter } from 'tanstack-fetch'
import {
  createAuthInterceptor,
  createRetryIdempotentInterceptor,
  createSseResumeInterceptor,
  createSsrForwardInterceptor,
  createStatusInterceptor,
  createTraceInterceptor,
} from 'tanstack-fetch/plugins'

export const api = createFetch({
  baseUrl: 'https://api.example.com',
  interceptors: [
    createAuthInterceptor({ getToken: () => getAccessToken() }),
    createStatusInterceptor({
      401: () => logout(),
      429: ({ context }) => ({
        action: 'retry',
        delayMs: parseRetryAfter(context.response?.headers, 2000),
      }),
    }),
    createTraceInterceptor(),
    createSsrForwardInterceptor(),
    createRetryIdempotentInterceptor(),
    createSseResumeInterceptor(),
    {
      name: 'locale',
      order: 25,
      onRequest: (context) => {
        context.request.headers.set('accept-language', 'fa')
        return { action: 'continue', context }
      },
    },
  ],
})

api.use('audit', {
  onResponse: (context) => {
    console.log(context.response?.status, context.request.url.pathname)
    return { action: 'continue', context }
  },
})

See Plugins & interceptors · Rate limit (429).

Live connections (tanstack-fetch/live) ​

The same options (baseUrl, headers, getToken / auth, plugins, interceptors, status handlers) apply to api.sse, api.ws, api.transport and api.stream. Two extra client options plug in the socket implementation:

OptionFor
createWebSocketapi.ws on runtimes without a global WebSocket (Node < 22), e.g. the ws package
createWebTransportapi.transport with a WebTransport polyfill outside browsers
ts
import WS from 'ws'
import { createFetch } from 'tanstack-fetch/live'

export const api = createFetch({
  baseUrl: 'https://api.example.com',
  getToken: () => getAccessToken(),
  createWebSocket: (url, { protocols, headers }) => new WS(url, protocols, { headers }) as never,
})

Per-connection options (authIn, reconnect, protocols, transport, webTransport) are in WebSocket, WebTransport & streams.

MIT Licensed · Created by Mohammad Garmabi · Not an official TanStack package