Published at

Integrating tRPC with Svelte Query in a Svelte 5 project

Integrating tRPC with Svelte Query in a Svelte 5 project

Do you want to integrate tRPC in your Svelte 5 project but feel like it's too much hassle? I've got you covered! This is a step-by-step guide to integrate tRPC with Svelte Query in a Svelte 5 project.

Table of Contents

What is my goal with this article?

If you found this article, you probably already know that tRPC is a tool for building type-safe APIs with TypeScript. I like it, because it provides a really pleasant framework for you to build an API in a SvelteKit application.

I have first used it in a large Next.js application, and fell in love with the ease with which it allows you to create new API endpoints, authenticated and with validation built in, and fully typesafe! For example, Create T3 App has it integrated out of the box, with Tanstack Query included. It is ready to use on client and on server.

For its SvelteKit equivalent, tRPC-SvelteKit, the setup is not that easy. The integration with Svelte Query is even more complex, and not described in the documentation. Moreover, it is not updated for Svelte 5 syntax.

In this article, I am aiming to provide a comprehensive, step-by-step guide to integrate tRPC with your new Svelte 5 project and Svelte Query.

1. Install the necessary libraries

First, let’s install the required dependencies:

npm install@trpc/server @trpc/client trpc-sveltekit @tanstack/svelte-query zod superjson

Let’s go over the packages:

  • @trpc/server and @trpc/client: The core libraries for building a tRPC server and client.
  • trpc-sveltekit: The library for integrating tRPC with SvelteKit.
  • @tanstack/svelte-query: The library for building a Svelte Query client.
  • zod: The library for building a Zod schema, which we will use for validation of inputs (and outputs) of our API endpoints.
  • superjson: The library for serializing and deserializing JSON data. This enables sending more complex data types (like Dates, Maps, Sets, etc.) over the network.

2. Create the initial file structure

If you are using Svelte 5, you should already have the src/lib directory. We will use it to store the tRPC file structure. You can use any other directory, it will just change the import paths.

First start with creating the src/lib/trpc/t.ts file. This file will let us easily import the tRPC instance anywhere in our project.

src/lib/trpc/t.ts
import { initTRPC } from "@trpc/server";
import type { Context } from "./context";
import { SuperJSON } from "superjson";
/*
* This is a helper function to create a tRPC instance.
* It is used as a singleton to avoid creating a new instance on every request.
*/
export const t = initTRPC.context<Context>().create({ transformer: SuperJSON })

Next, create the src/lib/trpc/context.ts file. This file will create a context for the tRPC server request and response handlers.

src/lib/trpc/context.ts
import type { RequestEvent } from '@sveltejs/kit';
/**
* Create a context for the tRPC server.
* This will pass the event object to all your request handlers.
*/
export async function createContext(event: RequestEvent) {
return {
event
};
}
export type Context = Awaited<ReturnType<typeof createContext>>;

This will create a context for the tRPC server that will be used to create the API endpoints.

Next, I recommend creating a src/lib/trpc/router directory, and inside it create the index.ts file. You could just create a src/lib/trpc/router.ts file, but I like to organize the sub-routers in a separate subdirectory.

src/lib/trpc/router/index.ts
import { usersRouter } from './users';
import { t } from '../t';
/**
* The main tRPC router that combines all sub-routers and procedures
*/
export const router = t.router({
greeting: t.procedure.query(async () => {
return `Hello tRPC! ${new Date().toLocaleTimeString()}`;
})
});
/**
* Factory function to create a caller for the router
* This is used to call procedures from server-side code
*/
export const createCaller = t.createCallerFactory(router);
export type Router = typeof router;

In this file, you can add all the sub-routers you need. For example, we can create a posts sub-router. And you should! As your application grows, you will probably need to split the API into many multiple routers. I like to do it based on the schema model, such as users, posts, comments, etc.

src/lib/trpc/router/posts.ts
import { t } from "../t";
export const postsRouter = t.router({
list: t.procedure.query(async () => {
return ["Post 1", "Post 2", "Post 3"];
})
});

And then add it to the main router:

src/lib/trpc/router/index.ts
// ...rest of the router
import { postsRouter } from "./posts";
export const router = t.router({
// ...rest of the router
posts: postsRouter,
});

This way you can create a readable and maintainable structure for your tRPC API.

Of course, this is just a way I like to structure it. The files can be placed in a different way, depending on your preferences, they would just be imported differently.

3. Create the Svelte Query client

Now, let’s create the Svelte Query client. I wholly recommend reading the Svelte Query documentation to get a good understanding of how it works.

Let’s create the src/lib/trpc/client.ts file:

src/lib/trpc/client.ts
import type { Router } from '$lib/trpc/router';
import { httpBatchLink } from '@trpc/client';
import { svelteQueryWrapper } from 'trpc-svelte-query-adapter';
import { createTRPCClient } from 'trpc-sveltekit';
import type { QueryClient } from '@tanstack/svelte-query';
import { PUBLIC_TRPC_SERVER_URL } from '$env/static/public';
const client = createTRPCClient<Router>({
links: [
httpBatchLink({
url: PUBLIC_TRPC_SERVER_URL
})
]
});
/**
* Create a Svelte Query client instance
*
* This function creates a Svelte Query client instance. You will import this client
* in your Svelte components to make API calls.
*/
export const api = (queryClient?: QueryClient) =>
svelteQueryWrapper<Router>({ client, queryClient });

This will create a tRPC client that will be used to make type-safe API calls to the server.

Now, you can use the api function in your Svelte components to make API calls:

src/routes/+page.svelte
<script lang="ts">
import { api } from '$lib/trpc/client';
// Create a query for the greeting endpoint
const greeting = api().greeting.createQuery()
</script>
<div>
{#if $greeting.data}
<!-- "Hello tRPC! 12:00:00" -->
<p>{$greeting.data}</p>
{/if}
</div>

4. Loading server-side page data with tRPC

For server-side data loading, you can use the load function in your SvelteKit page, via an accompanying src/routes/+page.server.ts file. For more information on how to use it, check out the SvelteKit documentation. You have two options here:

  1. Create a separate server side api handler, which will call the router function directly, instead of using RPC protocol. Personally, this is my preferred way, because it is more flexible and easier to debug.
  2. Use Svelte Query’s createServerQuery function to preload the data for the later used client side query.

I will show you both ways, and you can decide which one to use.

4.1. Creating a separate server side api function

Let’s start with the first option.

First, create a new file called $lib/trpc/server.ts. From here we will import the api function and call the router functions directly.

src/lib/trpc/server.ts
import { createCaller } from "./router";
import { createContext } from "./context";
import type { RequestEvent } from "@sveltejs/kit";
/**
* Create a tRPC server instance
*
* This is for use on page load, for the client side we want to use Svelte Query.
*/
export const getApi = async (event: RequestEvent) => createCaller(await createContext(event))

Now, we can use this function in our page to load the data:

src/routes/+page.server.ts
import type { PageServerLoad } from './$types';
import { getApi } from '$lib/trpc/server';
export const load: PageServerLoad = async (event) => {
const api = await getApi(event)
return {
greeting: await api.greeting(),
}
};

Now, we can use this data in our page. It will be in the props of the page component.

src/routes/+page.svelte
<script lang="ts">
const { data } = $props()
</script>
<div>
{#if data.greeting}
<!-- "Hello tRPC! 12:00:00" -->
<p>{data.greeting}</p>
{/if}
</div>

4.2. Using Svelte Query’s createServerQuery function

Now, let’s see the second option. This option leverages Svelte Query’s createServerQuery function to preload the data for the later used client side query.

For this, we can use the client side api function to create a query. We will preload data in the load function of the page.

src/routes/+page.server.ts
import { api } from '$lib/trpc/client';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async (event) => {
return {
query: await api().greeting.createServerQuery()
}
}

Now, we can use this query in our page. It will be in the props of the page component. Add it to src/routes/+page.svelte:

src/routes/+page.svelte
<script lang="ts">
const { data } = $props()
const greeting = data.greeting()
</script>
<div>
{#if $greeting.data}
<!-- "Hello tRPC! 12:00:00" -->
<p>{$greeting.data}</p>
{/if}
</div>

5. Conclusion

Now, you should have a working integration of tRPC with Svelte Query in your Svelte 5 project, and you can easily extend it to support any needs of your application!

If there is anything missing, or something is not clear, please let me know in the comments below!