Skip to main content

Set up with SvelteKit

Try using a file structure like this one!

.
├── prisma # <-- if prisma is added
│ └── [..]
├── src
│ │
│ ├── hooks.server.ts # <-- option 1: tRPC HTTP handler in +hooks.server.ts
│ │
│ ├── routes
│ │ ├── +layout.ts # <-- create new client for the user
│ │ ├── +layout.svelte # <-- set the query client in context
│ │ ├── api
│ │ │ └── trpc
│ │ │ └── [...trpc]
│ │ │ └── +server.ts # <-- option 2: tRPC HTTP handler in +server.ts (GET + POST)
│ │ └── [..]
│ └── lib
│ ├── server
│ │ ├── context # <-- create app context
│ │ ├── trpc # <-- tRPC server init + procedure helpers
│ │ └── routers
│ │ ├── post.ts # <-- sub routers
│ │ └── index.ts # <-- exports a merged router
│ │
│ ├── trpc.ts # <-- typesafe tRPC client + svelte-query hooks
│ └── [..]
└── [..]
tip

You can put the tRPC handler in either src/hooks.server.ts, or any +server.ts file that follows rest parameters.

Personally, I like the ability to isolate specific tRPC routes to their own handler file. SvelteKit hooks are generally intended to be used for app-wide controls, like auth sessions.

Add tRPC to existing SvelteKit project

1. Install deps

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

2. Enable strict mode

If you want to use Zod for input validation, make sure you have enabled strict mode in your tsconfig.json:

tsconfig.json
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
+ "strict": true
}

If strict mode is too harsh, you'll at least want to enable strictNullChecks:

tsconfig.json
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
+ "strictNullChecks": true
}

3. Create a tRPC router

Initialize your tRPC backend in src/lib/server/trpc.ts using the initTRPC function, and create your first router. We're going to make a simple "hello world" router and procedure here - but for deeper information on creating your tRPC API you should refer to:

View sample backend
src/lib/server/trpc.ts
import { initTRPC } from '@trpc/server';

// Avoid exporting the entire t-object since it's not very descriptive.
// For instance, the use of a t variable is common in i18n libraries.
const t = initTRPC.create();

// Base router and procedure helpers
export const router = t.router;
export const procedure = t.procedure;

src/lib/server/trpc/routers/index.ts
import { z } from 'zod';
import { procedure, router } from '../trpc';

export const appRouter = router({
hello: procedure
.input(
z.object({ text: z.string() }),
)
.query(({ input }) => {
return {
greeting: `hello ${input.text}`,
};
}),
});

// export type definition of API
export type AppRouter = typeof appRouter;

src/routes/api/trpc/[...trpc]/+server.ts
import { createTRPCRequestHandler } from '@bevm0/trpc-sveltekit'
import { appRouter } from '$lib/server/trpc/routers';
import type { AppRouter } from '$lib/server/trpc/routers';
import type { RouteParams, RouteId } from './$types'

/**
* export GET and POST SvelteKit request handlers
* @see https://trpc.io/docs/api-handler
* @see https://kit.svelte.dev/docs/routing#server
*/

const requestHandler = createTRPCRequestHandler<AppRouter, RouteParams, RouteId>({
router: appRouter,
createContext: (opts, event) => ({ opts, event })
})

export const GET = requestHandler
export const POST = requestHandler

4. Create tRPC hooks

use the createTRPCSvelte function to create a set of strongly-typed hooks from your API's type signature.

note

This tRPC client should only be used in components because it wasn't assigned a queryClient. It will invoke useQueryClient or refer to context to find it..

Keep on reading to see how to initialize a tRPC client for load functions.

src/lib/trpc.ts
import { httpBatchLink } from '@trpc/client';
import { createTRPCSvelte } from '@bevm0/trpc-svelte';
import type { AppRouter } from '$lib/server/trpc/routers';

export const trpc = createTRPCSvelte<AppRouter>({
links: [
httpBatchLink({ url: 'http://localhost:5173/api/trpc' }),
],
});

5. Configure +layout.ts

tip

The app's query client should be created per user, i.e. when they load the website for the first time. Creating it in +layout.ts ensures that we don't initialize a query client on the server and repeatedly share the same query client with all users.

Furthermore, the client initialized in +layout.ts will use SvelteKit's special fetch function (which fixes buggy double fetching for some reason), and since we explicitly provided a query client, it will directly invoke that query client instead of calling useQueryClient.

src/routes/+layout.ts
import { httpBatchLink } from '@trpc/client';
import { createTRPCSvelte } from '@bevm0/trpc-svelte';
import { QueryClient } from '@tanstack/svelte-query'
import type { AppRouter } from '$lib/server/trpc/routers';

export const load: LayoutLoad = async (event) => {
const queryClient = new QueryClient()

const trpc = createTRPCSvelte<AppRouter>({
links: [
httpBatchLink({
url: 'http://localhost:5173/api/trpc',
fetch: event.fetch
}),
],
}, { svelteQueryContext: queryClient });

return { trpc }
}

6. Setup +layout.svelte

src/routes/+layout.svelte
<script lang="ts">
import { QueryClientProvider } from '@tanstack/svelte-query'
import type { PageData } from './$types'

export let data: PageData

data.trpc.setContext(data.trpc.client, data.trpc.queryClient)
</script>

<QueryClientProvider client={data.trpc.queryClient}>
<slot />
</QueryClientProvider>

note

You can't use createQuery, createMutation, etc. in +layout.svelte because there isn't any QueryClientProvider wrapping this component. I'm not sure if there's an idiomatic way to solve this limitation.

tip

The tRPC proxy created in +layout.ts and src/lib/trpc.ts are different clients. The purpose of the one in +layout.ts is to be used in load functions to hydrate a new query client created for the user. Once the page loads, the initialized query client is used instead of initializing a new one.

Now the client in src/lib/trpc.ts takes over. setContext is used to share the data already created, to be used by helper functions from trpc.getContext(). i.e. all tRPC clients are able to share the same queryClient through the use of Svelte's context API.

The tRPC context also requires an untyped tRPC client to make its requests, which is provided by trpc.client.

7. Make an API request from a component

You're all set!

You can now use the hooks you have just created to invoke your API.

note
  1. We initialized a new query client for the user in +layout.ts -> When load functions invoke this tRPC client, it will be caching directly to the newly initialized query client

  2. We used that query client in the QueryClientProvider in +layout.svelte and also added it to the trpc context. -> When setContext was called, a new proxy was created to perform helper functions on the specified query client.

src/routes/+page.svelte
<script lang="ts">
import { trpc } from '$lib/trpc';

const hello = trpc.hello.createQuery({ text: 'client' });
</script>

{#if $hello.data}
<div>
<p>{$hello.data.greeting}</p>
</div>
{:else}
<div>Loading...</div>;
{/if}

8. Prefetch an API request from a load function

note

I don't think you can initialize a tRPC + svelte-query client in +layout.server.ts or +page.server.ts

tip

context is intended to work like getContext, but outside of components. It is always available as long as you call createTRPCSvelte and provide the svelteQueryContext (query client) property in the second argument.

src/routes/+page.ts
import type { PageLoad } from './$types'

export const load: PageLoad = async ({ parent }) => {
// use the initialized tRPC client from the parent +layout.ts
const { trpc } = await parent()

// in components, if you didn't specify the `svelteQueryContext` in `src/lib/trpc.ts` -- i.e. like the guide,
// you should use `getContext()` because `context` wasn't able to be computed.
return {
greeting: trpc.context.hello.fetch({ text: 'client' });
}
}

Usage in the component can vary:

  1. Create a matching query -> since this was already fetched and cached, no initial fetch needs to occur
  2. Just use the data without any query -> depending on the helper function invoked, you can just use the returned data
tip

Differences between types of helper fetch variants (including their infinite counterparts)

  • fetch: Gets the data, caches the result, and returns the result
  • prefetch: Gets the data, caches the result, but doesn't return the data (it returns void)
  • ensure: First checks if the query client already has the data; if not, then it gets the data, caches the result, and returns the result. (The latter part is identical to fetch)

There's so many ways to do almost the same thing! It seems that ensure would be the best option since it does everything "optimally".

Usually fetch would be sufficient, but if you have lots of queries that can be replicated in subroutes, i.e. src/routes/+layout.ts and /src/routes/+page.ts or their children have duplicate queries, then ensure might be ideal because it doesn't refetch those queries.

src/routes/+page.svelte
<script lang="ts">
import { trpc } from '$lib/trpc'
import { PageData } from './$types'

// data = {
// hello: '...'
// }
export let data: PageData

const hello = trpc.hello.createQuery({ text: 'client' });
</script>

<!-- You can also just use the data -->
<p>The greeting was {data.greeting}</p>

<!-- Since the data was already fectched, the "else" block should never appear -->
{#if $hello.data}
<div>
<p>{$hello.data.greeting}</p>
</div>
{:else}
<div>Loading...</div>;
{/if}

Experimental

Since tRPC is initialized in +layout.ts and returned. You should actually be able to access it via the page store. You'll just need to define it as part of global PageData in src/app.d.ts

<script lang="ts">
import { page } from '$app/stores'

const trpc = $page.data.trpc

const utils = trpc.getContext()

const query = trpc.greeting.createQuery()
</script>

{...}

Now you can completely ditch the src/lib/trpc.ts file!