I maintain tRPC-SvelteKit, a small library for building end-to-end typesafe APIs in SvelteKit apps with tRPC. I just released version 4.1, and its main feature is first-class support for TanStack Svelte Query.

In this post I'll show what it does and how to set it up, including server-side rendering.

The problem

tRPC gives you typed procedures. TanStack Query gives you caching, background refetching, mutations and invalidation. To use them together, every query needs a query key and a query function, and every mutation needs a mutation function. Writing those by hand for each procedure is repetitive, and it's easy to get a key wrong.

There are community adapters for this, but the ones I know of target older versions. trpc-svelte-query-adapter supports tRPC v10 and Svelte Query v5. trpc-svelte-query supports tRPC v11, but with SvelteKit 2 and Svelte Query v5. I wanted something for tRPC v11, SvelteKit 3 and Svelte Query v6, so I added it to tRPC-SvelteKit itself.

What's new: createTRPCQueryOptions

createTRPCQueryOptions(client) takes a tRPC client and gives you back an object that mirrors your router. For each procedure you get:

  • Queries: queryOptions(input, options), queryKey(input) and pathKey()
  • Mutations: mutationOptions(options), mutationKey() and pathKey()
  • Routers: pathKey(), which matches every query and mutation under that router

A few things I cared about while building it:

  • No dependency on any TanStack Query package. It only builds plain option objects, which you pass to createQuery and createMutation.
  • Types come from the tRPC client, so inputs and outputs match exactly, transformers included.
  • The query keys use the same format as tRPC's own TanStack Query integration, e.g. [['authors', 'list'], { input: 'smith', type: 'query' }].
  • It works with Svelte Query v6 and its runes API: createQuery(() => options).

Here's the short version:

const api = createTRPCQueryOptions(trpc(page)); const authors = createQuery(() => api.authors.list.queryOptions('smith')); const save = createMutation(() => api.authors.save.mutationOptions({ onSuccess: () => queryClient.invalidateQueries({ queryKey: api.authors.pathKey() }) }) ); 

Setup

Install the packages (tRPC-SvelteKit 4 requires @trpc/server and @trpc/client v11):

npm i trpc-sveltekit@4.1 @tanstack/svelte-query 

I'm assuming you already have a trpc() client helper, as in the Getting started guide:

// src/lib/trpc/client.ts import type { Router } from '$lib/trpc/router'; import { createTRPCClient, type TRPCClientInit } from 'trpc-sveltekit'; type TRPCClient = ReturnType<typeof createTRPCClient<Router>>; let browserClient: TRPCClient; export function trpc(init?: TRPCClientInit): TRPCClient { const isBrowser = typeof window !== 'undefined'; if (isBrowser && browserClient) return browserClient; const client = createTRPCClient<Router>({ init }); if (isBrowser) browserClient = client; return client; } 

Next to it, define a helper that builds the query options:

// src/lib/trpc/query.ts import { trpc } from '$lib/trpc/client'; import { createTRPCQueryOptions, type TRPCClientInit } from 'trpc-sveltekit'; export const api = (init?: TRPCClientInit) => createTRPCQueryOptions(trpc(init)); 

Then create a QueryClient in a layout load function and provide it to your pages. Queries only run in the browser. The data needed for server-side rendering is prefetched in load functions, as shown in the next section:

// src/routes/+layout.ts import { browser } from '$app/env'; import { QueryClient } from '@tanstack/svelte-query'; import type { LayoutLoad } from './$types'; export const load: LayoutLoad = () => ({ queryClient: new QueryClient({ defaultOptions: { queries: { enabled: browser } } }) }); 
<!-- src/routes/+layout.svelte --> <script lang="ts"> import { QueryClientProvider } from '@tanstack/svelte-query'; let { data, children } = $props(); </script> <QueryClientProvider client={data.queryClient}> {@render children()} </QueryClientProvider> 

Queries, rendered on the server

Prefetch the query in a +page.ts load function, so the page is rendered with data on the server:

// src/routes/+page.ts import { api } from '$lib/trpc/query'; import type { PageLoad } from './$types'; export const load: PageLoad = async (event) => { const { queryClient } = await event.parent(); await queryClient.prefetchQuery(api(event).greeting.queryOptions()); }; 

Then use the same options in the page. The browser reuses the server response while hydrating instead of fetching again. queryOptions(input, options) accepts any createQuery option, such as staleTime, and adds the queryKey and queryFn:

<!-- src/routes/+page.svelte --> <script lang="ts"> import { page } from '$app/state'; import { api } from '$lib/trpc/query'; import { createQuery } from '@tanstack/svelte-query'; const greeting = createQuery(() => api(page).greeting.queryOptions(undefined, { staleTime: 10_000 }) ); </script> <p>{greeting.data}</p> 

Mutations and invalidation

mutationOptions(options) does the same for createMutation. To refetch queries after a mutation, invalidate them with a key:

  • queryKey(input) matches a single query
  • pathKey() matches every query of a procedure, for any input, or of a whole router
<!-- src/routes/authors/+page.svelte --> <script lang="ts"> import { page } from '$app/state'; import { api } from '$lib/trpc/query'; import { createMutation, createQuery, useQueryClient } from '@tanstack/svelte-query'; const queryClient = useQueryClient(); let search = $state(''); // the input is part of the query key, so changing `search` loads (and caches) new results const authors = createQuery(() => api(page).authors.list.queryOptions(search)); const save = createMutation(() => api(page).authors.save.mutationOptions({ // refetch every `authors` query (for any input) once the mutation succeeds onSuccess: () => queryClient.invalidateQueries({ queryKey: api(page).authors.pathKey() }) }) ); </script> <input type="search" bind:value={search} /> <ul> {#each authors.data ?? [] as author (author.id)} <li>{author.firstName} {author.lastName}</li> {/each} </ul> <button aria-busy={save.isPending} onclick={() => save.mutate({ id: null, firstName: 'Jane', lastName: 'Doe', bio: null })} > Add author </button> 

One limitation: subscriptions aren't supported, since TanStack Query doesn't handle them. You can still use them directly through the tRPC client.

Also in 4.1

Svelte Query is the main story, but the release also includes:

  • fixes for the WebSocket support, which is still experimental: the production server now shuts down cleanly on SIGTERM/SIGINT, and connections made before a router is attached are refused, so clients retry
  • @sveltejs/adapter-bun verified to work, including streaming and subscriptions over server-sent events
  • llms.txt and llms-full.txt for AI assistants and coding agents
  • a changelog

There are no breaking changes. If you're on 4.0, upgrading is just a version bump.

Links

If you try it, I'd like to hear how it goes. Issues and feedback are welcome on GitHub.