# tRPC-SvelteKit > tRPC-SvelteKit is a tRPC adapter that makes it easy to build end-to-end typesafe APIs for your SvelteKit applications. It provides a single SvelteKit `handle` hook that serves your tRPC router over HTTP and works with every SvelteKit adapter (Node, Bun, Vercel, Netlify, Cloudflare, …), a `createTRPCClient` function to call your procedures from pages and `load` functions, typed TanStack Svelte Query options and query keys (`createTRPCQueryOptions`), support for setting cookies and headers from procedures, streamed responses and subscriptions over server-sent events, and experimental WebSocket support for `@sveltejs/adapter-node`. Documentation website: https://icflorescu.github.io/trpc-sveltekit/ Repository: https://github.com/icflorescu/trpc-sveltekit Changelog: https://github.com/icflorescu/trpc-sveltekit/blob/main/CHANGELOG.md npm package: https://www.npmjs.com/package/trpc-sveltekit ## Key facts - `trpc-sveltekit` v4 (current version: 4.1.0) requires tRPC v11 (`@trpc/server` and `@trpc/client` `^11.0.0`) and works with SvelteKit 2 or 3 (`@sveltejs/kit` `^2.0.0 || ^3.0.0`) on Node.js `>=20`. Install it with `pnpm add trpc-sveltekit @trpc/server @trpc/client`. It doesn’t depend on Svelte itself; the examples use Svelte 5 with runes. Use `trpc-sveltekit@3` for tRPC v10 (SvelteKit 1 and 2) and `trpc-sveltekit@2` for tRPC v9 (SvelteKit 1). - The main entry point (`trpc-sveltekit`) exports `createTRPCHandle`, used in your server hooks, `createTRPCClient`, used in your pages and `load` functions, and `createTRPCQueryOptions`, for TanStack Svelte Query. It also exports their types (`CreateTRPCHandleOptions`, `TRPCHandle`, `CreateTRPCClientOptions`, `TRPCClientInit`, `ValidRoute`). The experimental WebSocket APIs are exported separately, from `trpc-sveltekit/websocket`. - `createTRPCHandle({ router, createContext })` returns a SvelteKit `handle` hook: export it as `handle` from `src/hooks.server.ts`. Don’t create a `+server.ts` endpoint (such as `src/routes/trpc/[...trpc]/+server.ts`) for tRPC. To combine it with your own hooks, use `sequence` from `@sveltejs/kit/hooks` (e.g. `export const handle = sequence(authHandle, trpcHandle)`); a hook that populates `event.locals` for your context must come before the tRPC handle. The handle also accepts the options of tRPC’s HTTP handlers (`onError`, `responseMeta`, `batching`, `allowBatching`, `allowMethodOverride`) with their tRPC v11 signatures. - `createContext` receives the SvelteKit `RequestEvent` as its first argument (so you can read `event.cookies`, `event.locals`, `event.request.headers` …) and the tRPC fetch adapter options (`req`, `resHeaders`, `info`) as its second; it may be async. Declare the context type as `Awaited>` and pass it to `initTRPC.context().create()`. - The tRPC API is served under the `/trpc` path prefix by default (procedure `authors.list` is at `/trpc/authors.list`). The prefix is reserved, so don’t put SvelteKit routes under it. Change it with the `url` option of `createTRPCHandle` (it must start with `/` and must not end with `/`), and pass the same `url` to `createTRPCClient`. - Create clients with `createTRPCClient` from `trpc-sveltekit`, not the function of the same name from `@trpc/client` (which requires `links`). By default it uses an `httpBatchLink` pointing at `/trpc` and passes any other `httpBatchLink` option through (`transformer`, `headers`, `maxURLLength`, `methodOverride` …). You can pass your own `links` instead (then `url` and `init` aren’t accepted). - On the server, `createTRPCClient` throws unless you pass `init`: a SvelteKit load event, or anything with a `url` and optionally a `fetch` function. With the event of a universal `load` function, the client uses SvelteKit’s `fetch`, which forwards cookies and, during server-side rendering, calls your `handle` directly instead of making an HTTP request. In the browser `init` is optional; components usually pass `page` from `$app/state`. The docs use a `trpc(init?)` helper in `src/lib/trpc/client.ts` that reuses a single client in the browser and creates a new one for each call on the server, where every request has its own `fetch`. - Page data vs page server data: in a universal `load` function (`+page.ts`, `+layout.ts`), which runs **both** on the server and in the browser, call `await trpc(event).some.procedure.query()`. In server-only code (`+page.server.ts`, `+layout.server.ts`, form actions), skip HTTP and use a server-side caller: export `const createCaller = t.createCallerFactory(router)` next to your router, then call `await createCaller(await createContext(event)).some.procedure(input)` (procedures are called as functions, without `.query()` or `.mutate()`). - Data transformers (such as superjson, or `trpc-transformer`, which adds Decimal.js support): in tRPC v11 they’re set on links rather than on the client. Use the same transformer on the server (`initTRPC.context().create({ transformer })`) and on the client (`createTRPCClient({ init, transformer })` forwards it to its default link; with custom `links`, set it on each link). - Procedures can set response headers and cookies through the request event, if you put it in the context: `ctx.event.setHeaders({ … })`, `ctx.event.cookies.set(…)` and `ctx.event.cookies.delete(…)`. The handle copies them onto the tRPC response. Set cookies with `cookies`, not with `setHeaders` (SvelteKit throws on `set-cookie`). When streaming, only the headers and cookies set before the response starts are sent. - Responses are streamed, so `httpBatchStreamLink` and subscriptions over server-sent events (`httpSubscriptionLink`, combined with `splitLink`; subscriptions can be written as async generators) work with every SvelteKit adapter. Prefer them over the experimental WebSocket support unless you need WebSockets. - TanStack Svelte Query (v6, `createQuery(() => options)`): `createTRPCQueryOptions(client)` returns a proxy mirroring your router, e.g. `api.authors.list.queryOptions(input, { staleTime })`, `api.authors.save.mutationOptions({ onSuccess })`, `api.authors.list.queryKey(input)` and `api.authors.pathKey()` (to invalidate every query of a procedure or router). The first argument of `queryOptions` is always the input (`undefined` for procedures without input). Types come from the tRPC client, transformers included. Don’t use `trpc-svelte-query-adapter` with v4: it only supports tRPC v10 and Svelte Query v5. - The handle doesn’t read `url.searchParams`, so it can be used in apps with prerendered pages; procedure calls made from prerendered `load` functions run at build time. - Experimental WebSocket support (`trpc-sveltekit/websocket`) works with `@sveltejs/adapter-node` only and needs the optional `ws` peer dependency. It has four parts: the `vitePluginTrpcWebSocket` Vite plugin (or `trpcWebSocketPlugin({ path })`) for the dev and preview servers; a `wsServer.js` production entry point next to `package.json` calling `SvelteKitTRPCWSServer(import.meta.url)`, which you start the app with (`node ./wsServer.js` instead of `node build`); `createTRPCWebSocketServer({ router, createContext })` in `hooks.server.ts`, guarded with `if (!building)`; and `createTRPCWebSocketClient()` on the client. Its `createContext` receives the `CreateWSSContextFnOptions` of `@trpc/server/adapters/ws` (`{ req, res, info }`), not a SvelteKit request event: read cookies from `req.headers.cookie`, or a token from `info.connectionParams` (set with the client’s `connectionParams` option). `createTRPCWebSocketClient` returns `undefined` on the server, so only use the client in the browser (event handlers, `$effect`). All procedures go over the WebSocket unless you build your own client with a `splitLink`. On `SIGTERM`/`SIGINT`, `SvelteKitTRPCWSServer` closes the WebSocket connections so the process can exit; with a custom server, call `closeWSSGlobalInstance()` yourself. - Upgrading from v3 (tRPC v10): v4 is mostly API-compatible. Upgrade `@trpc/server` and `@trpc/client` to v11 and follow the tRPC v11 migration guide (https://trpc.io/docs/migrate-from-v10-to-v11). `createContext` now also receives the fetch adapter options as a second argument; `responseMeta` and `onError` use the tRPC v11 signatures; `createTRPCClient` forwards `transformer` and every `httpBatchLink` option to its link; `@sveltejs/adapter-node` is no longer a peer dependency, `ws` is only needed for WebSocket support, and the `cookie` package is no longer a dependency. - The examples use SvelteKit 3 conventions: `#lib/...` subpath imports (declared in the `imports` field of `package.json`) instead of `$lib/...`, `building` from `$app/env` and private environment variables from `$app/env/private`. In a SvelteKit 2 app, import from `$lib/...`, get `building` from `$app/environment` and private environment variables from `$env/static/private` (or `$env/dynamic/private`). ## About the examples The code examples come from the example apps in the repository (https://github.com/icflorescu/trpc-sveltekit/tree/main/examples): `simple` (the basics, and TanStack Svelte Query), `bookstall` (authentication, CRUD, Prisma) and `websocket` (subscriptions over WebSockets). Each example file is labeled with its path in the example app. They use a few things that aren’t part of tRPC-SvelteKit, which you should replace with your own: - in `bookstall`: the Prisma client imported from `#lib/server/prisma.ts` (a SQLite database through `@prisma/adapter-better-sqlite3`) and its models; JWT handling with `jsonwebtoken` and a `JWT_SECRET` environment variable; a deliberately naive MD5 password hash (`hash-wasm`); Zod input validation; the `trpc-transformer` data transformer; a `logger` middleware; and UI components from `#lib/components` (`DataTable`, `ModalEditor`, `TextInput`, `TextareaInput`, `AuthorizationAlert`); - in `simple`: `delay` from `node:timers/promises`, used to simulate a slow procedure; the Svelte Query mutation snippet uses an `authors` router like the one in `bookstall`; - in all of them: Pico CSS markup (such as `role="button"` and `aria-busy`). Snippets without a file label are written for the documentation and may use variables defined elsewhere (such as `Router`, `init` or `getToken`). ## Getting started Source: https://icflorescu.github.io/trpc-sveltekit/getting-started Install the `trpc-sveltekit` package and its dependencies: ```sh pnpm add trpc-sveltekit @trpc/server @trpc/client # or: npm i trpc-sveltekit @trpc/server @trpc/client # or: yarn add trpc-sveltekit @trpc/server @trpc/client # or: bun add trpc-sveltekit @trpc/server @trpc/client ``` `trpc-sveltekit` v4 requires `@trpc/server` and `@trpc/client` v11 and works with SvelteKit 2 and 3. If you’re coming from v3, read the upgrade guide (https://icflorescu.github.io/trpc-sveltekit/upgrading-to-v4). ### 1. Create your tRPC router See https://trpc.io/docs/server/routers. Exporting a caller factory lets you call procedures directly on the server (see the "Page server data" section below): `src/lib/trpc/router.ts` (`simple` example): ```ts import type { Context } from '#lib/trpc/context.ts'; import { initTRPC } from '@trpc/server'; import { setTimeout as delay } from 'node:timers/promises'; export const t = initTRPC.context().create(); export const router = t.router({ greeting: t.procedure.query(async () => { await delay(500); // 👈 simulate an expensive operation return `Hello tRPC v11 @ ${new Date().toLocaleTimeString()}`; }) }); export const createCaller = t.createCallerFactory(router); export type Router = typeof router; ``` ### 2. Create a tRPC context See https://trpc.io/docs/server/context. The function receives the SvelteKit request event: `src/lib/trpc/context.ts` (`simple` example): ```ts import type { RequestEvent } from '@sveltejs/kit'; export async function createContext(event: RequestEvent) { return { event // 👈 `event` is now available in your context }; } export type Context = Awaited>; ``` ### 3. Add the handle to your SvelteKit server hooks See https://svelte.dev/docs/kit/hooks: `src/hooks.server.ts` (`simple` example): ```ts import { createContext } from '#lib/trpc/context.ts'; import { router } from '#lib/trpc/router.ts'; import { createTRPCHandle } from 'trpc-sveltekit'; export const handle = createTRPCHandle({ router, createContext }); ``` If you have your own logic to place in the server hook, combine the handles with the `sequence` helper function (https://svelte.dev/docs/kit/@sveltejs-kit-hooks#sequence). ### 4. Define a helper function to use the tRPC client in your pages It reuses a single client in the browser; on the server, it creates a new client for each call, using the `init` (load event) it receives: `src/lib/trpc/client.ts` (`simple` example): ```ts import type { Router } from '#lib/trpc/router.ts'; import { createTRPCClient, type TRPCClientInit } from 'trpc-sveltekit'; type TRPCClient = ReturnType>; let browserClient: TRPCClient; export function trpc(init?: TRPCClientInit): TRPCClient { const isBrowser = typeof window !== 'undefined'; if (isBrowser && browserClient) return browserClient; const client = createTRPCClient({ init }); if (isBrowser) browserClient = client; return client; } ``` ### 5. Call the tRPC procedures in your pages `src/routes/+page.svelte` (`simple` example): ```svelte
Loading data in
+page.svelte
Load

{greeting}

``` ### 6. Optionally, use TanStack Svelte Query If you want caching, background refetching and invalidation, use `createTRPCQueryOptions` to turn your client into typed TanStack Svelte Query options and query keys (see the "Using with Svelte Query" section below). ### Remember The `trpc-sveltekit` package exports three functions: `createTRPCHandle`, used in your SvelteKit server hooks; `createTRPCClient`, used in your pages and `load` functions; and `createTRPCQueryOptions`, which turns your client into typed TanStack Svelte Query options and query keys (see the "Using with Svelte Query" section below). ## Upgrading to v4 Source: https://icflorescu.github.io/trpc-sveltekit/upgrading-to-v4 `trpc-sveltekit` v4 targets tRPC v11 and works with SvelteKit 2 and 3. The API is mostly unchanged, so upgrading is usually a matter of updating your dependencies: - Upgrade `@trpc/server` and `@trpc/client` to v11 and follow the tRPC v11 migration guide (https://trpc.io/docs/migrate-from-v10-to-v11). - `createTRPCClient` still accepts a `transformer`, and forwards it to its default link. If you pass your own `links`, set the transformer on them instead. - `createTRPCClient` now forwards every `httpBatchLink` option, such as `maxURLLength` or `methodOverride`. - `createTRPCHandle` accepts every option of tRPC’s HTTP handlers. `responseMeta` and `onError` use the tRPC v11 signatures, and `createContext` receives the fetch adapter options as a second argument. - Responses are streamed, so `httpBatchStreamLink` and subscriptions over server-sent events work with every adapter. - The handle can be used in apps with prerendered pages. - `@sveltejs/adapter-node` is no longer a peer dependency, `ws` is only needed for WebSocket support, and `trpc-sveltekit` no longer depends on the `cookie` package. - If you used the `trpc-svelte-query-adapter` package, which only supports tRPC v10, switch to `createTRPCQueryOptions` (available since v4.1.0): see the "Using with Svelte Query" section. ## Page data Source: https://icflorescu.github.io/trpc-sveltekit/page-data Assuming you created a `trpc()` helper function as described in the "Getting started" section, you can call tRPC procedures in your SvelteKit page load functions (https://svelte.dev/docs/kit/load). Pass the load `event` to the helper: `src/routes/page-data/+page.ts` (`simple` example): ```ts import { trpc } from '#lib/trpc/client.ts'; import type { PageLoad } from './$types'; // 👇 this method will be invoked on BOTH the server and the client, as needed ⚠️ export const load: PageLoad = async (event) => ({ greeting: await trpc(event).greeting.query() }); ``` You can then use the loaded data in your pages like so: `src/routes/page-data/+page.svelte` (`simple` example): ```svelte
Data loaded in
+page.ts

{data.greeting}

``` A load function defined in a `+page.ts` file runs **both on the server and in the browser**. You should therefore consider using a `+page.server.ts` instead (see the next section). Read https://svelte.dev/docs/kit/load#Universal-vs-server to learn the difference between universal and server data loading. Using TanStack Svelte Query? Prefetch your queries in `+page.ts` load functions, with the same typed options your pages use (see the "Using with Svelte Query" section). ## Page server data Source: https://icflorescu.github.io/trpc-sveltekit/page-server-data To call tRPC procedures in your SvelteKit page server load functions (https://svelte.dev/docs/kit/load), bypass HTTP and invoke them directly on a tRPC caller (https://trpc.io/docs/server/server-side-calls), created with the `createCaller` factory exported by your router and the context built from the request event: `src/routes/page-server-data/+page.server.ts` (`simple` example): ```ts import { createContext } from '#lib/trpc/context.ts'; import { createCaller } from '#lib/trpc/router.ts'; import type { PageServerLoad } from './$types'; // 👇 since this is only called on the server, we can bypass HTTP 💡 export const load: PageServerLoad = async (event) => ({ greeting: await createCaller(await createContext(event)).greeting() }); ``` You can then use the loaded data in your pages like so: `src/routes/page-server-data/+page.svelte` (`simple` example): ```svelte
Data loaded in
+page.server.ts

{data.greeting}

``` Read https://svelte.dev/docs/kit/load#Universal-vs-server to learn the difference between universal and server data loading. If you need to cache, refetch or invalidate data in the browser, consider TanStack Svelte Query (see the next section). ## Using with Svelte Query Source: https://icflorescu.github.io/trpc-sveltekit/using-with-svelte-query tRPC-SvelteKit works great with TanStack Svelte Query (https://tanstack.com/query/latest/docs/framework/svelte/overview). Its `createTRPCQueryOptions` function turns your tRPC client into typed query and mutation options, so you don’t have to write query keys or query functions yourself. It only builds plain option objects, so it doesn’t depend on any TanStack Query package. Install Svelte Query with `pnpm add @tanstack/svelte-query`. ### Setup Define a helper next to your `trpc()` client helper (see the "Getting started" section): `src/lib/trpc/query.ts` (`simple` example): ```ts import { trpc } from '#lib/trpc/client.ts'; import { createTRPCQueryOptions, type TRPCClientInit } from 'trpc-sveltekit'; // 👇 TanStack Query options for every procedure, built on the `trpc()` client helper export const api = (init?: TRPCClientInit) => createTRPCQueryOptions(trpc(init)); ``` 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: `src/routes/svelte-query/+layout.ts` (`simple` example): ```ts import { browser } from '$app/env'; import { QueryClient } from '@tanstack/svelte-query'; import type { LayoutLoad } from './$types'; export const load: LayoutLoad = () => ({ // 👇 queries only run in the browser; data needed for SSR is prefetched in `load` functions queryClient: new QueryClient({ defaultOptions: { queries: { enabled: browser } } }) }); ``` `src/routes/svelte-query/+layout.svelte` (`simple` example): ```svelte {@render children()} ``` ### Queries Prefetch your queries in `load` functions, so that pages are rendered with data on the server. The browser then reuses the server response while hydrating instead of fetching again: `src/routes/svelte-query/+page.ts` (`simple` example): ```ts import { api } from '#lib/trpc/query.ts'; import type { PageLoad } from './$types'; export const load: PageLoad = async (event) => { const { queryClient } = await event.parent(); // 👇 prefetch the query, so that the page is rendered with data on the server await queryClient.prefetchQuery(api(event).greeting.queryOptions()); }; ``` And use the same options in your pages. `queryOptions(input, options)` accepts any `createQuery` option, such as `staleTime`, and adds the `queryKey` and `queryFn` (the first argument is always the input: pass `undefined` for procedures without input): `src/routes/svelte-query/+page.svelte` (`simple` example): ```svelte
Loading data with
@tanstack/svelte-query
Refresh

{greeting.data}

``` ### 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`: ```svelte
    {#each authors.data ?? [] as author (author.id)}
  • {author.firstName} {author.lastName}
  • {/each}
``` The query keys have the same format as the ones of tRPC’s own TanStack Query integration, e.g. `[['authors', 'list'], { input: 'smith', type: 'query' }]`. Subscriptions aren’t supported, since TanStack Query doesn’t handle them. See the complete Svelte Query example in the repository: https://github.com/icflorescu/trpc-sveltekit/tree/main/examples/simple/src/routes/svelte-query ### Third-party adapters for older versions At the time of writing, the community-maintained tRPC Svelte Query Adapter (https://github.com/vishalbalaji/trpc-svelte-query-adapter) only supports tRPC v10 and Svelte Query v5, so it requires `trpc-sveltekit` v3. If you’re using SvelteKit 2 and Svelte Query v5, trpc-svelte-query (https://github.com/ottomated/trpc-svelte-query) supports tRPC v11, with its own SvelteKit integration. ## Suggested structure Source: https://icflorescu.github.io/trpc-sveltekit/suggested-structure Here’s how you could structure the tRPC-related files in a non-trivial application, in order to avoid cyclic dependencies: ```text -------------- | lib/trpc | 👈 place your tRPC-related stuff here -------------- | |-- context.ts 👈 router context |-- t.ts 👈 tRPC definition helper (see below) |-- router.ts 👈 main router definition (imports files defined in routes folder) |-- middleware.ts 👈 tRPC middleware (this could be split into multiple files if necessary) |-- client.ts 👈 define a client helper function here | | ------------ |--| routes | 👈 split your routes into multiple files and place them here | ------------ | | | |-- route-1.ts | |-- route-2.ts | |-- route-3.ts | | ... | |-- route-n.ts | | ---------------- |--| middleware | 👈 split your middleware into multiple files and place them here ---------------- | |-- middleware-1.ts | ... |-- middleware-n.ts ``` The `t.ts` file contains a simple helper that allows you to build routes and middleware: `src/lib/trpc/t.ts` (`bookstall` example): ```ts import type { Context } from '#lib/trpc/context.ts'; import { initTRPC } from '@trpc/server'; import transformer from 'trpc-transformer'; export const t = initTRPC.context().create({ transformer }); ``` You’ll import `t.ts` in multiple other files, such as `router.ts`, `middleware.ts`, `routes/route-1.ts`, `routes/route-2.ts` etc. ## Authentication Source: https://icflorescu.github.io/trpc-sveltekit/authentication tRPC-SvelteKit works with both cookie-based and JWT-based authentication. Below is an example of securing tRPC procedures with cookie-based authentication. Let’s assume you have a username/password login page that POSTs to the following form action (https://svelte.dev/docs/kit/form-actions), which sets a `jwt` cookie: `src/routes/login/+page.server.ts` (`bookstall` example): ```ts import { JWT_SECRET } from '$app/env/private'; import prisma from '#lib/server/prisma.ts'; import { fail } from '@sveltejs/kit'; import { md5 } from 'hash-wasm'; import jwt from 'jsonwebtoken'; import type { Actions } from './$types'; export const actions: Actions = { default: async ({ request, cookies }) => { try { const data = await request.formData(); const email = data.get('email') as string; const password = data.get('password') as string; // 👇 replace this with a non-naiive hashing algorithm const passwordHash = await md5(password); const { id, name } = await prisma.user.findFirstOrThrow({ where: { email, passwordHash }, select: { id: true, name: true } }); cookies.set('jwt', jwt.sign({ id, name }, JWT_SECRET), { path: '/', secure: false }); return { success: true }; // 👆 or, if we're using HTTP headers based auth, we could return the token, // and let the client set the header on subsequent requests } catch { return fail(401, { error: 'Authentication failed' }); } } }; ``` Your tRPC context builder could read and verify the token like this: `src/lib/trpc/context.ts` (`bookstall` example): ```ts import { JWT_SECRET } from '$app/env/private'; import type { RequestEvent } from '@sveltejs/kit'; import jwt from 'jsonwebtoken'; export async function createContext(event: RequestEvent) { try { const token = event.cookies.get('jwt'); // 👆 or, if we're using HTTP headers based authentication, we could do something like this: // const token = event.request.headers.get('authorization')?.replace('Bearer ', ''); const { id: userId } = jwt.verify(token || '', JWT_SECRET) as { id: string }; return { userId }; } catch { return { userId: '' }; } } export type Context = Awaited>; ``` You could define an authentication middleware like this: `src/lib/trpc/middleware/auth.ts` (`bookstall` example): ```ts import { t } from '#lib/trpc/t.ts'; import { TRPCError } from '@trpc/server'; export const auth = t.middleware(async ({ next, ctx }) => { if (!ctx.userId) throw new TRPCError({ code: 'UNAUTHORIZED' }); return next(); }); ``` And use the auth middleware in your tRPC procedures as needed: `src/lib/trpc/routes/authors.ts` (`bookstall` example): ```ts import prisma from '#lib/server/prisma.ts'; import { auth } from '#lib/trpc/middleware/auth.ts'; import { logger } from '#lib/trpc/middleware/logger.ts'; import { t } from '#lib/trpc/t.ts'; import { z } from 'zod'; export const authors = t.router({ list: t.procedure .use(logger) .input(z.string().optional()) .query(({ input }) => prisma.author.findMany({ select: { id: true, firstName: true, lastName: true, updatedAt: true, _count: { select: { books: true } } }, orderBy: { updatedAt: 'desc' }, where: input ? { OR: [{ firstName: { contains: input } }, { lastName: { contains: input } }] } : undefined }) ), loadOptions: t.procedure.use(logger).query(() => prisma.author .findMany({ select: { id: true, firstName: true, lastName: true }, orderBy: [{ firstName: 'asc' }, { lastName: 'asc' }] }) .then((authors) => authors.map(({ id, firstName, lastName }) => ({ label: `${firstName} ${lastName}`, value: id })) ) ), load: t.procedure .use(logger) .use(auth) // 👈 use auth middleware .input(z.string()) .query(({ input }) => prisma.author.findUniqueOrThrow({ select: { id: true, firstName: true, lastName: true, bio: true, updatedAt: true, updatedBy: { select: { name: true } } }, where: { id: input } }) ), save: t.procedure .use(logger) .use(auth) // 👈 use auth middleware .input( z.object({ id: z.string().nullable(), firstName: z.string().min(3).max(50), lastName: z.string().min(3).max(50), bio: z.string().nullable() }) ) .mutation(async ({ input: { id, ...rest }, ctx: { userId } }) => { if (id) { await prisma.author.update({ data: { ...rest, updatedByUserId: userId }, where: { id } }); } else { await prisma.author.create({ data: { ...rest, updatedByUserId: userId } }); } }), delete: t.procedure .use(logger) .use(auth) // 👈 use auth middleware .input(z.string()) .mutation(async ({ input: id }) => { await prisma.author.delete({ where: { id } }); }) }); ``` ## Handling errors Source: https://icflorescu.github.io/trpc-sveltekit/handling-errors Whenever an error occurs in a procedure, tRPC responds to the client with an object that includes an `error` property, containing all the information you need to handle the error on the client. Calls made with the tRPC client then throw a `TRPCClientError`. Here’s how you can handle it in a SvelteKit page (see the `try/catch` block in the `handleSave` function): `src/routes/authors/+page.svelte` (`bookstall` example): ```svelte Authors • Bookstall `${firstName} ${lastName}` }, { title: 'Books', align: 'right', accessor: (author) => author._count.books } ]} onadd={handleAdd} onedit={handleEdit} ondelete={handleDelete} />
(needsAuthorization = false)} /> ``` See the error handling page of the tRPC documentation (https://trpc.io/docs/server/error-handling) for more information. You can also supply an `onError` handler to `createTRPCHandle` in your `hooks.server.ts`, for instance to log all errors to a service like Sentry: `src/hooks.server.ts` (`bookstall` example): ```ts import { createContext } from '#lib/trpc/context.ts'; import { router } from '#lib/trpc/router.ts'; import { createTRPCHandle } from 'trpc-sveltekit'; export const handle = createTRPCHandle({ router, createContext, onError: ({ type, path, error }) => console.error(`Encountered error while trying to process ${type} @ ${path}:`, error) }); ``` ## Recipes and caveats Source: https://icflorescu.github.io/trpc-sveltekit/recipes-and-caveats ### Client options By default, `createTRPCClient` uses an `httpBatchLink` (https://trpc.io/docs/client/links/httpBatchLink) and passes any of its options through. For instance, you can set custom HTTP headers (such as an `Authorization` header), or limit the URL length of batched requests: ```ts createTRPCClient({ init, headers: () => ({ authorization: getToken() }), maxURLLength: 2083 }); ``` ### Custom links If you need full control, pass your own links (https://trpc.io/docs/client/links) instead. For example, to stream responses with `httpBatchStreamLink` and use subscriptions over server-sent events (https://trpc.io/docs/server/subscriptions), which work with every SvelteKit adapter: ```ts import { httpBatchStreamLink, httpSubscriptionLink, splitLink } from '@trpc/client'; const url = `${location.origin}/trpc`; createTRPCClient({ links: [ splitLink({ condition: (op) => op.type === 'subscription', true: httpSubscriptionLink({ url }), false: httpBatchStreamLink({ url }) }) ] }); ``` `createTRPCQueryOptions` accepts any tRPC client, including one created with custom links, so you can use it with TanStack Svelte Query as well (subscriptions excepted, since TanStack Query doesn’t handle them; see the "Using with Svelte Query" section). ### Setting response headers and cookies To change the headers of the response, use the `event` provided to `createContext`. Pass the `event` into the context, so that it can be accessed in your procedures. You can then use `event.setHeaders` and `event.cookies` to edit the headers; tRPC-SvelteKit copies them onto the tRPC response. When streaming responses, only the headers and cookies set before the response starts are sent. `src/lib/trpc/context.ts` (`simple` example) is shown in full in the "Getting started" section above. ### Using custom data transformers The `createTRPCClient` function optionally accepts a `transformer` option, which it passes to its link (in tRPC v11, transformers are set on links rather than on the client). See https://trpc.io/docs/server/data-transformers, and keep in mind that you must use the same `transformer` when defining the router and when creating the client. If you’re looking for a convenient transformer based on superjson (https://github.com/blitz-js/superjson) with Decimal.js (https://mikemcl.github.io/decimal.js/) support, consider the `trpc-transformer` package (https://github.com/icflorescu/trpc-transformer); you’ll have to install its `superjson` and `decimal.js` peer dependencies as well. ### Response caching Your server responses must satisfy some criteria in order to be cached (e.g. by Vercel’s Edge Network); see https://trpc.io/docs/server/caching. `createTRPCHandle` accepts all the options of tRPC’s HTTP handlers, including a `responseMeta` function. ### Inferring types It’s often useful to wrap your tRPC client calls in other functions, which requires inferring the input and output types of your router procedures. The `@trpc/server` package exports two helper types for this purpose: `inferRouterInputs` and `inferRouterOutputs` (see https://trpc.io/docs/client/vanilla/infer-types). To make your code faster to type and easier to read, you could refine them when defining your router: `src/lib/trpc/router.ts` (`bookstall` example): ```ts import { authors } from '#lib/trpc/routes/authors.ts'; import { books } from '#lib/trpc/routes/books.ts'; import { stores } from '#lib/trpc/routes/stores.ts'; import { t } from '#lib/trpc/t.ts'; import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; export const router = t.router({ authors, books, stores }); export const createCaller = t.createCallerFactory(router); export type Router = typeof router; // 👇 type helpers 💡 export type RouterInputs = inferRouterInputs; export type RouterOutputs = inferRouterOutputs; ``` Then use the helper types in your pages, as the authors page does with `RouterInputs` (see the types of `item` and `handleSave`): `src/routes/authors/+page.svelte` (`bookstall` example) is shown in full in the "Handling errors" section above. Using TanStack Svelte Query? `createTRPCQueryOptions` infers the input and output types of your query and mutation options from your router, so you don’t need these helper types there (see the "Using with Svelte Query" section). ### Reserved path prefix Invoking `createTRPCHandle` reserves the `/trpc` path prefix for the tRPC API, so you can’t use it for any other purpose. If you need it for something else, change the reserved prefix with the `url` option (and pass the same `url` to `createTRPCClient`). ### Prerendering The handle doesn’t read `url.searchParams`, so it can be used in apps with prerendered pages. Keep in mind that procedure calls made from prerendered `load` functions run at build time. ## WebSocket support (experimental) Source: https://icflorescu.github.io/trpc-sveltekit/experimental-websocket-support SvelteKit doesn’t offer WebSocket support (https://github.com/sveltejs/kit/issues/1491), but if you’re using `@sveltejs/adapter-node`, tRPC-SvelteKit can spin up an experimental WebSocket server to process tRPC procedure calls. If you only need subscriptions, consider streaming them over server-sent events instead (see "Custom links" in the "Recipes and caveats" section), which work with every adapter. ### Caveats - Works with `@sveltejs/adapter-node` exclusively. - All tRPC procedures are handled by the WebSocket server, unless you build your own client with a `splitLink` (https://trpc.io/docs/client/links/splitLink). - No WebSocket server is created when building or prerendering. ### Setup Install the package and its dependencies: ```sh pnpm add trpc-sveltekit @trpc/server @trpc/client ws pnpm add -D @sveltejs/adapter-node # or: npm i trpc-sveltekit @trpc/server @trpc/client ws && npm i -D @sveltejs/adapter-node ``` Add the Vite plugin, which handles WebSocket connections in development and preview. It uses the `/trpc` path; use `trpcWebSocketPlugin({ path })` for a different one. `vite.config.ts` (`websocket` example): ```ts import adapter from '@sveltejs/adapter-node'; import { sveltekit } from '@sveltejs/kit/vite'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePluginTrpcWebSocket } from 'trpc-sveltekit/websocket'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ sveltekit({ adapter: adapter(), preprocess: vitePreprocess() }), vitePluginTrpcWebSocket ] }); ``` Create the production server entry point next to `package.json`, and use it to start your app (e.g. `"start": "node ./wsServer.js"`). `SvelteKitTRPCWSServer` also accepts a second argument: `{ path, buildDir }`. `wsServer.js` (`websocket` example): ```js import { SvelteKitTRPCWSServer } from 'trpc-sveltekit/websocket'; SvelteKitTRPCWSServer(import.meta.url); ``` Attach your router to the WebSocket server in your server hooks. Since no WebSocket server exists while building, guard the call with `building`: `src/hooks.server.ts` (`websocket` example): ```ts import { building } from '$app/env'; import { createContext } from '#lib/trpc/context.ts'; import { router } from '#lib/trpc/router.ts'; import { createTRPCWebSocketServer } from 'trpc-sveltekit/websocket'; if (!building) createTRPCWebSocketServer({ router, createContext }); ``` The context receives the WebSocket connection options instead of a SvelteKit request event: `src/lib/trpc/context.ts` (`websocket` example): ```ts import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws'; export async function createContext(_opts: CreateWSSContextFnOptions) { return { // context information }; } export type Context = Awaited>; ``` ### Authentication Since there’s no SvelteKit request event, read the credentials from the upgrade request yourself. Browsers send your site’s cookies with it, so you can parse `req.headers.cookie` (for instance with the `cookie` package). Alternatively, pass a token with the `connectionParams` option of `createTRPCWebSocketClient` and read it from `info.connectionParams`: ```ts import type { CreateWSSContextFnOptions } from '@trpc/server/adapters/ws'; import { parse } from 'cookie'; export async function createContext({ req, info }: CreateWSSContextFnOptions) { // 👇 browsers send your site's cookies with the WebSocket upgrade request const { session } = parse(req.headers.cookie ?? ''); // 👇 or read a token passed to `createTRPCWebSocketClient({ connectionParams })` const token = info.connectionParams?.token; return { user: await getUser(session ?? token) }; } ``` ### Usage Define your router as usual; subscriptions can be written as async generators: `src/lib/trpc/router.ts` (`websocket` example): ```ts import type { Context } from '#lib/trpc/context.ts'; import { initTRPC } from '@trpc/server'; import { EventEmitter, on } from 'node:events'; export const t = initTRPC.context().create(); const ee = new EventEmitter(); export const router = t.router({ allMessages: t.procedure.subscription(async function* ({ signal }) { for await (const [message] of on(ee, 'add', { signal })) { yield `${new Date().toLocaleTimeString()}: ${message as string}`; } }), addMessage: t.procedure .input((input: unknown) => { if (typeof input === 'string' && input.length > 0) return input; throw new Error('Invalid input type'); }) .mutation(({ input: message }) => { ee.emit('add', message); return { success: true }; }) }); export type Router = typeof router; ``` Create the client with `createTRPCWebSocketClient`, which also accepts the `wsLink` and `createWSClient` options (`transformer`, `retryDelayMs`, `lazy` …) and a `url` path. It returns `undefined` on the server, so only use it in the browser: `src/lib/trpc/client.ts` (`websocket` example): ```ts import type { Router } from '#lib/trpc/router.ts'; import { createTRPCWebSocketClient } from 'trpc-sveltekit/websocket'; let browserClient: ReturnType>; export function trpc() { browserClient ??= createTRPCWebSocketClient(); return browserClient!; } ``` Then call your procedures and subscribe to subscriptions in your pages: `src/routes/messages/+page.svelte` (`websocket` example): ```svelte

✨ WebSocket Messages ✨

{#each messages as message, i (i)} {#if i === messages.length - 1}

{message}

{:else}

{message}

{/if} {/each}
``` ### How it works - The Vite plugin creates a `WebSocketServer` (with `noServer: true`, so it doesn’t interfere with Vite’s own HMR WebSocket) and upgrades the requests made to the tRPC path. - The server is stored in `globalThis[Symbol.for('trpc.sveltekit.wss')]`, so it can be reached from your SvelteKit server hooks. - Connections are refused until your hooks attach a router with `createTRPCWebSocketServer`, and the client keeps retrying. In development, SvelteKit only loads your server hooks on the first HTTP request, so a client that reconnects after a restart of the dev server connects once a page is loaded. - In production, `SvelteKitTRPCWSServer` imports the `adapter-node` build output and attaches the same upgrade handler to its HTTP server. - On `SIGTERM` and `SIGINT`, `SvelteKitTRPCWSServer` closes the WebSocket connections (with code 1001, so clients reconnect), letting `adapter-node` shut down gracefully. If you start the server yourself, call `closeWSSGlobalInstance()` when shutting down. - In the browser, `trpc-sveltekit/websocket` resolves to a client-only entry point, so no server code ends up in your bundle. See the complete websocket example in the repository: https://github.com/icflorescu/trpc-sveltekit/tree/main/examples/websocket