Skip to content

Svelte / SvelteKit fetch vs tanstack-fetch ​

October 12, 2026 · Svelte · SvelteKit · HTTP

Stack

Svelte 4+ / SvelteKit · tanstack-fetch/svelte · @tanstack/svelte-query

SvelteKit’s event.fetch is perfect inside load. The browser still needs a typed client for Query, mutations, and SSE with Authorization.

tanstack-fetch/svelte uses setFetchClient in a layout, useFetch in components, and useSse stores ($data, $status). Kit SSR gets ssr-forward via the migrate scaffold’s api.server.ts.

This post is the deep dive: architecture, side-by-side code, graphs, migrate path, SSE, tRPC, and when to keep the default stack client.

TL;DR ​

If you need…Prefer
Browser Query + storestanstack-fetch/svelte
Kit load with cookiesapi.server.ts + ssr-forward
SSE storesuseSse
load-only pages, no client cacheevent.fetch is fine
Share client with React monoreposame createFetch
bash
npm install tanstack-fetch svelte @tanstack/svelte-query

Mental model ​

Split server (+page.server.ts / api.server.ts) from browser (setFetchClient in +layout.svelte). Do not open SSE in server load.

Contract that matters for Query:

  1. Success → return the parsed body
  2. HTTP failure → throw (so isError works)
  3. Cancel → pass Query’s signal through

Side-by-side ​

event.fetch only (glue when client needs cache) ​

ts
// +page.server.ts
export const load = async ({ fetch }) => {
  const users = await fetch('/api/users').then((r) => r.json())
  return { users }
}

Hydrating into Svelte Query later means a second client and duplicated types.

setFetchClient + Svelte Query ​

svelte
<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { createFetch } from 'tanstack-fetch'
  import { setFetchClient } from 'tanstack-fetch/svelte'

  const api = createFetch({
    baseUrl: import.meta.env.VITE_API_URL,
    getToken: () => localStorage.getItem('access_token'),
  })
  setFetchClient({ client: api })
</script>

<slot />
svelte
<script lang="ts">
  import { useFetch } from 'tanstack-fetch/svelte'
  import { createQuery } from '@tanstack/svelte-query'
  import { isFetchError } from 'tanstack-fetch'

  const api = useFetch()
  const users = createQuery({
    queryKey: ['users'],
    queryFn: ({ signal }) => api.get<User[]>('/users', { signal }),
  })
</script>

{#if $users.isPending}
  <p>Loading…</p>
{:else if $users.isError}
  <p>{isFetchError($users.error) ? $users.error.status : 'Error'}</p>
{:else}
  <ul>
    {#each $users.data ?? [] as user}
      <li>{user.name}</li>
    {/each}
  </ul>
{/if}

Bootstrap / wiring ​

SvelteKit SSR scaffold:

bash
npx tanstack-fetch migrate --from fetch --framework sveltekit --write

Creates src/lib/api.ts (browser) and src/lib/api.server.ts (ssr-forward + cookies) for load functions.

Comparison graph: where glue accumulates ​

Rough relative boilerplate (lower is better) — a map of where teams spend time, not a micro-benchmark.

Feature matrix ​

Capabilitytanstack-fetchevent.fetch / raw fetch
setFetchClient / useFetchYesDIY
useSse storesYesDIY
Kit SSR helpermigrate api.serverManual
Svelte Query fitNativeAdapt
Typed errorsFetchErrorDIY
Refresh pluginYesDIY
SSE in +page.serverNo — browser onlyN/A

Auth, status handlers, refresh ​

ts
import { createFetch } from 'tanstack-fetch'
import { createRefreshTokenInterceptor } from 'tanstack-fetch/plugins'

let expiresAt = 0

const api = createFetch({
  baseUrl: '/api',
  getToken: () => localStorage.getItem('access_token'),
})

api.use(
  'refresh-token',
  createRefreshTokenInterceptor({
    refresh: async () => {
      const body = await api.post<{ accessToken: string; expiresIn: number }>('/auth/refresh', {
        interceptors: { eject: ['refresh-token', 'auth'] },
      })
      localStorage.setItem('access_token', body.accessToken)
      expiresAt = Date.now() + body.expiresIn * 1000
    },
    before: { getExpiresAt: () => expiresAt, skewMs: 60_000 },
    after: { enabled: true },
  }),
)

The plugin keeps one in-flight refresh and retries the failed call — the usual interceptor queue, without the queue code.

Mutations ​

ts
import { createMutation, useQueryClient } from '@tanstack/svelte-query'
import { useFetch } from 'tanstack-fetch/svelte'

const api = useFetch()
const queryClient = useQueryClient()
const createUser = createMutation({
  mutationFn: (input: CreateUserInput) => api.post<User>('/users', { body: input }),
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
})

Live data: SSE ​

Browser EventSource cannot set Authorization. tanstack-fetch/sse streams over fetch.

svelte
<script lang="ts">
  import { createFetch } from 'tanstack-fetch/sse'
  import { useSse } from 'tanstack-fetch/svelte'

  const api = createFetch({ plugins: ['sse-resume'] })
  const { data, status, error, close } = useSse<OrderEvent>('/orders/stream', { client: api })
</script>

{#if $status === 'error'}
  <p>{$error?.message}</p>
{:else}
  <p>{$status}</p>
  <pre>{JSON.stringify($data, null, 2)}</pre>
  <button type="button" on:click={close}>Stop</button>
{/if}

Status: connecting | connected | disconnected | error.

tRPC over the same client ​

ts
import { createTRPCClient, httpBatchLink } from '@trpc/client'
import { createTRPCFetch } from 'tanstack-fetch/trpc'
import { createQuery } from '@tanstack/svelte-query'
import { api } from './api'
import type { AppRouter } from './server'

const trpc = createTRPCClient<AppRouter>({
  links: [httpBatchLink({ url: '/api/trpc', fetch: createTRPCFetch(api) })],
})

export const postsQuery = () =>
  createQuery({
    queryKey: ['posts'],
    queryFn: ({ signal }) => trpc.post.list.query(undefined, { signal }),
  })

Migrate ​

bash
npx tanstack-fetch migrate --from fetch --framework svelte --write
npx tanstack-fetch migrate --from fetch --framework sveltekit --write
npx tanstack-fetch doctor --dir ./src

Suggested folder layout ​

text
src/
  lib/api.ts
  lib/api.server.ts     # Kit SSR
  routes/+layout.svelte # setFetchClient
  routes/+page.svelte
  lib/queries/users.ts

Decision tree ​

  1. Is TanStack Query your source of truth for server state?
    • Yes → start with tanstack-fetch
    • No → event.fetch may stay
  2. Do you need SSE with Authorization or resume?
    • Yes → tanstack-fetch/sse + useSse
    • No → either client works for simple REST
  3. Do other frameworks share this backend client?
    • Yes → one createFetch across packages
    • No → still fine; adapters are optional

Checklist ​

  • [ ] setFetchClient in root layout
  • [ ] Kit: api.server.ts for load cookies
  • [ ] Forward signal in queryFns
  • [ ] SSE only in browser components
  • [ ] Doctor clean

Try it ​

bash
npm install tanstack-fetch svelte @tanstack/svelte-query
npx tanstack-fetch doctor --dir ./src

Series ​

Same deep-dive format for every stack:

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