Skip to content

SSR ​

Forward cookies and auth headers from the incoming server request. Same createFetch client in the browser — ssr-forward skips when source is 'browser'.

Live demo ​

Framework

Next.js (App Router) · simulated server incoming in the browser

Live demo · Next.js SSR

ssr-forward cookie / auth

Real createFetch with source: 'ssr' + ssr-forward — watch headers get attached (then skipped in browser).

Request log

Run server prefetch to see forwarded Cookie / Authorization.

// app/users/page.tsx (Next.js App Router)
import { cookies } from 'next/headers'
import { createFetch } from 'tanstack-fetch'
import { createSsrForwardInterceptor, createTraceInterceptor } from 'tanstack-fetch/plugins'

const createServerApi = async () =>
  createFetch({
    baseUrl: process.env.API_URL!,
    source: 'ssr',
    interceptors: [createSsrForwardInterceptor(), createTraceInterceptor()],
    incoming: async () => ({
      cookie: (await cookies()).toString(),
    }),
  })

await queryClient.query({
  queryKey: ['users'],
  queryFn: async () => (await createServerApi()).get('/users'),
})

Next.js ​

Framework

React · Next.js App Router · @tanstack/react-query

Use an absolute baseUrl on the server and the ssr-forward interceptor.

ts
import { createFetch } from 'tanstack-fetch'
import {
  createRetryIdempotentInterceptor,
  createSsrForwardInterceptor,
  createTraceInterceptor,
} from 'tanstack-fetch/plugins'
import { cookies } from 'next/headers'

export const api = createFetch({
  baseUrl: process.env.API_URL, // absolute in SSR
  source: 'ssr',
  interceptors: [
    createSsrForwardInterceptor(),
    createTraceInterceptor(),
    createRetryIdempotentInterceptor(),
  ],
  incoming: async () => {
    const jar = await cookies()
    return { cookie: jar.toString() }
  },
})

Prefetch + hydrate ​

tsx
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { usersQueryOptions } from '@/queries/users'

export default async function Page() {
  const queryClient = new QueryClient()
  await queryClient.query(usersQueryOptions)
  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <UsersClient />
    </HydrationBoundary>
  )
}

Example: Next.js SSR · repo: examples/next-ssr · Blog: Next.js fetch vs tanstack-fetch

Why cookies don't forward by default ​

Next.js App Router fetch() runs in a server context with a clean request — browser cookies are not piped automatically. Without ssr-forward, your server queryFn sees no auth and returns 401 or empty data, even though the browser has the cookie.

This is the most common SSR failure for TanStack Query + Next.js apps.


Nuxt ​

Framework

Vue · Nuxt 3 / 4 · @tanstack/vue-query

Create a server-aware client in a Nuxt plugin (or a shared server/utils module). Read cookies from useRequestHeaders on the server.

ts
// plugins/tanstack-fetch.ts
import { createFetch } from 'tanstack-fetch'
import {
  createRetryIdempotentInterceptor,
  createSsrForwardInterceptor,
  createTraceInterceptor,
} from 'tanstack-fetch/plugins'
import { createFetchPlugin } from 'tanstack-fetch/vue'

export default defineNuxtPlugin((nuxtApp) => {
  const config = useRuntimeConfig()

  const api = createFetch({
    baseUrl: config.public.apiBase as string,
    source: import.meta.server ? 'ssr' : 'browser',
    interceptors: [
      createSsrForwardInterceptor(),
      createTraceInterceptor(),
      createRetryIdempotentInterceptor(),
    ],
    incoming: () => {
      if (!import.meta.server) return undefined
      const headers = useRequestHeaders(['cookie', 'authorization', 'x-request-id'])
      return {
        cookie: headers.cookie,
        authorization: headers.authorization,
        requestId: headers['x-request-id'],
      }
    },
  })

  nuxtApp.vueApp.use(createFetchPlugin({ client: api }))
  return { provide: { api } }
})

Prefetch + hydrate (Vue Query) ​

vue
<!-- pages/users.vue -->
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { useFetch as useFetchClient } from 'tanstack-fetch/vue'

const api = useFetchClient()

const { data, suspense } = useQuery({
  queryKey: ['users'],
  queryFn: ({ signal }) => api.get<User[]>('/users', { signal }),
})

// Prefetch on the server; the state is dehydrated into the payload below
await suspense()
</script>

Typical Nuxt + Vue Query setup:

ts
// plugins/vue-query.ts
import { VueQueryPlugin, QueryClient, dehydrate, hydrate } from '@tanstack/vue-query'

export default defineNuxtPlugin((nuxtApp) => {
  const queryClient = new QueryClient()

  if (import.meta.server) {
    nuxtApp.hooks.hook('app:rendered', () => {
      nuxtApp.payload.vueQueryState = dehydrate(queryClient)
    })
  }

  if (import.meta.client) {
    hydrate(queryClient, nuxtApp.payload.vueQueryState)
  }

  nuxtApp.vueApp.use(VueQueryPlugin, { queryClient })
})

Example: Nuxt SSR · Vue helpers: Vue & Nuxt · Blog: Nuxt $fetch vs tanstack-fetch

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