Set up with SvelteKit
Recommended file structure
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
│ └── [..]
└── [..]
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
- yarn
- pnpm
npm install @trpc/server @trpc/client @tanstack/react-query zod @bevm0/trpc-svelte-query @bevm0/trpc-sveltekit
yarn add @trpc/server @trpc/client @tanstack/react-query zod @bevm0/trpc-svelte-query @bevm0/trpc-sveltekit
pnpm add @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
:
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
+ "strict": true
}
If strict mode is too harsh, you'll at least want to enable strictNullChecks
:
"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:
- the Quickstart guide and Backend usage docs for tRPC information
View sample backend
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;
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;
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.
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.
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
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
.
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
<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>
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.
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.
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 clientWe used that query client in the QueryClientProvider in
+layout.svelte
and also added it to the trpc context. -> WhensetContext
was called, a new proxy was created to perform helper functions on the specified query client.
<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
I don't think you can initialize a tRPC + svelte-query client in +layout.server.ts
or +page.server.ts
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.
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:
- Create a matching query -> since this was already fetched and cached, no initial fetch needs to occur
- Just use the data without any query -> depending on the helper function invoked, you can just use the returned data
Differences between types of helper fetch variants (including their infinite counterparts)
fetch
: Gets the data, caches the result, and returns the resultprefetch
: Gets the data, caches the result, but doesn't return the data (it returnsvoid
)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 tofetch
)
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.
<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!