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)andpathKey() -
Mutations:
mutationOptions(options),mutationKey()andpathKey() -
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
createQueryandcreateMutation. - 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-bunverified to work, including streaming and subscriptions over server-sent events -
llms.txtandllms-full.txtfor 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.
Top comments (0)