How to use TRPC with SvelteKit
A basic trpc example with sveltekit
Table of Contents
Most code is based on this blog post and adapted to svelte 5 and the new getRequestEvent function.
What is trpc?
tRPC allows you to easily build & consume fully typesafe APIs without schemas or code generation. tRPC is for full-stack TypeScript developers. It makes it easy to write endpoints that you can safely use in both the front and backend of your app. Type errors with your API contracts will be caught at build time, reducing the surface for bugs in your application at runtime.
Setup
Install dependencies
pnpm i @trpc/client @trpc/server @trpc/server zod -D
Create TRPC context
src/lib/server/context.ts
import { initTRPC } from '@trpc/server';
import type { FetchCreateContextFnOptions } from '@trpc/server/adapters/fetch';
export function createSvelteKitContext(locals: App.Locals) {
return function (opts: FetchCreateContextFnOptions) {
return { locals };
};
}
export const t = initTRPC.context<ReturnType<typeof createSvelteKitContext>>().create();
export const router = t.router;
export const publicProcedure = t.procedure;
Create TRPC router
src/lib/server/router.ts
import { z } from 'zod';
import { publicProcedure, router } from './context';
let name = 'Name';
export const appRouter = router({
name: router({
get: publicProcedure.query(() => name),
set: publicProcedure.input(z.object({ name: z.string() })).mutation(({ input }) => {
name = input.name;
return name;
})
})
});
export type AppRouter = typeof appRouter;
Create TRPC client
src/lib/trpc.ts
import { httpBatchLink, createTRPCClient } from '@trpc/client';
import type { AppRouter } from './server/router';
import { getRequestEvent } from '$app/server';
export const trpc = createTRPCClient<AppRouter>({
links: [httpBatchLink({ url: '/api/trpc' })]
});
export function trpcOnServer() {
const { fetch } = getRequestEvent();
return createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: '/api/trpc',
fetch
})
]
});
}
Create TRPC server/endpoint
src/routes/api/trpc/[...procedure]/+server.ts
import { createSvelteKitContext } from '$lib/server/context';
import { appRouter } from '$lib/server/router';
import type { RequestHandler } from '@sveltejs/kit';
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
const handleTRPC: RequestHandler = function (event) {
return fetchRequestHandler({
req: event.request,
router: appRouter,
endpoint: '/api/trpc',
createContext: createSvelteKitContext(event.locals)
});
};
export const GET = handleTRPC;
export const POST = handleTRPC;
Usage
Usage on the server
src/routes/+page.server.ts
import { trpcOnServer } from '$lib/trpc';
export const load = async () => {
const trpc = trpcOnServer();
return { name: await trpc.name.get.query() };
};
Usage on the client
src/routes/+page.svelte
<script lang="ts">
import { browser } from '$app/environment';
import { trpc } from '$lib/trpc';
let { data } = $props();
let name = $state(data.name);
let serverName = $derived(browser ? trpc.name.get.query() : data.name);
</script>
<input
bind:value={name}
onkeyup={async () => {
await trpc.name.set.mutate({ name });
serverName = trpc.name.get.query();
}}
/>
{#await serverName}
<p>Loading...</p>
{:then value}
<p>{value}</p>
{:catch error}
<p>{error}</p>
{/await}
Middleware
Create a middleware
Make sure to define locals.user
in src/hooks.server.ts
to make the middleware work.
src/lib/server/middleware.ts
import { TRPCError } from '@trpc/server';
import { t } from './context';
export const authed = t.middleware(({ ctx, next }) => {
if (ctx.locals.user) {
return next();
// next can also modify the context or modify the incoming input by passing data to next()
}
throw new TRPCError({
code: 'UNAUTHORIZED',
message: 'You must be logged in to do this'
});
});
Use the middleware
src/lib/server/router.ts`
import { publicProcedure, router } from './context';
import { authed } from './middleware';
export const appRouter = router({
name: router({
auth: publicProcedure.use(authed).query(() => 'Auth passed')
})
});
export type AppRouter = typeof appRouter;