Documentation available at icflorescu.github.io/trpc-sveltekit.
Move fast and break nothing.
End-to-end typesafe APIs for your
SvelteKit applications.
✅ @sveltejs/adapter-node
✅ @sveltejs/adapter-vercel
✅ @sveltejs/adapter-netlify
tRPC-SvelteKit v3.x.x is compatible with tRPC v10.
If you're using tRPC v9, use tRPC-SvelteKit v2.x.x. The old source code is available in the trpc-v9 branch.
yarn add trpc-sveltekit @trpc/server @trpc/client
Create your tRPC router:
// lib/trpc/router.ts
import type { Context } from '$lib/trpc/context';
import { initTRPC } from '@trpc/server';
import delay from 'delay';
export const t = initTRPC.context<Context>().create();
export const router = t.router({
greeting: t.procedure.query(async () => {
await delay(500); // 👈 simulate an expensive operation
return `Hello tRPC v10 @ ${new Date().toLocaleTimeString()}`;
})
});
export type Router = typeof router;
Create a tRPC context:
// lib/trpc/context.ts
import type { RequestEvent } from '@sveltejs/kit';
import type { inferAsyncReturnType } from '@trpc/server';
// we're not using the event parameter is this example,
// hence the eslint-disable rule
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export async function createContext(event: RequestEvent) {
return {
// context information
};
}
export type Context = inferAsyncReturnType<typeof createContext>;
Add this handle to your SvelteKit app hooks:
// hooks.server.ts
import { createContext } from '$lib/trpc/context';
import { router } from '$lib/trpc/router';
import type { Handle } from '@sveltejs/kit';
import { createTRPCHandle } from 'trpc-sveltekit';
export const handle: Handle = createTRPCHandle({ router, createContext });
// lib/trpc/client.ts
import type { Router } from '$lib/trpc/router';
import { createTRPCClient, type TRPCClientInit } from 'trpc-sveltekit';
let browserClient: ReturnType<typeof createTRPCClient<Router>>;
export function trpc(init?: TRPCClientInit) {
const isBrowser = typeof window !== 'undefined';
if (isBrowser && browserClient) return browserClient;
const client = createTRPCClient<Router>({ init });
if (isBrowser) browserClient = client;
return client;
}
// routes/+page.svelte
<script lang="ts">
import { page } from '$app/stores';
import { trpc } from '$lib/trpc/client';
let greeting = 'press the button to load data';
let loading = false;
const loadData = async () => {
loading = true;
greeting = await trpc($page).greeting.query();
loading = false;
};
</script>
<h6>Loading data in<br /><code>+page.svelte</code></h6>
<a
href="#load"
role="button"
class="secondary"
aria-busy={loading}
on:click|preventDefault={loadData}>Load</a
>
<p>{greeting}</p>
This repository contains a handful of examples:
(courtesy of @SrZorro)
SvelteKit doesn't (yet) offer WebSockets support, but if you're using @sveltejs/adapter-node
, tRPC-SvelteKit
can spin up an experimental WS server to process tRPC procedure calls (see the implementation details to find out how this works under the hood).
- Works with @sveltejs/adapter-node exclusively;
- The URL is hardcoded to
/trpc
; - When in websocket mode, all tRPC methods are handled by it; this could be changed at some point so that only
subscriptions
are handled by the WebSockets server; - Prerendering is not supported, since in the current implementation no WebSockets server is created when building/prerendering.
yarn add trpc-sveltekit @trpc/server @trpc/client @sveltejs/adapter-node ws
In your vite.config.ts
, add:
import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig } from 'vite';
import { vitePluginTrpcWebSocket } from 'trpc-sveltekit/websocket'; // ➕
const config: UserConfig = {
plugins: [
sveltekit(),
vitePluginTrpcWebSocket // ➕
]
};
export default config;
In your svelte.config.js
, modify:
import adapter from '@sveltejs/adapter-node'; // ➕
// import adapter from '@sveltejs/adapter-auto'; // ➖
Create this file next to package.json
your server entrypoint:
// wsServer.js
import { SvelteKitTRPCWSServer } from "trpc-sveltekit/websocket";
SvelteKitTRPCWSServer(import.meta.url);
In your package.json
scripts
, modify the start
command:
{
"scripts": {
"start": "node ./wsServer"
}
}
Call this function from your SvelteKit app server hooks:
// hooks.server.ts
import { createContext } from '$lib/trpc/context';
import { router } from '$lib/trpc/router';
import { createTRPCWebSocketServer } from "trpc-sveltekit/websocket";
import { building } from '$app/environment';
if (!building) createTRPCWebSocketServer({ router, createContext })
// lib/trpc/client.ts
import type { Router } from '$lib/trpc/router';
import { createTRPCWebSocketClient } from "trpc-sveltekit/websocket";
let browserClient: ReturnType<typeof createTRPCWebSocketClient<Router>>;
export function trpc() {
const isBrowser = typeof window !== 'undefined';
if (isBrowser && browserClient) return browserClient;
const client = createTRPCWebSocketClient<Router>();
if (isBrowser) browserClient = client;
return client;
}
// routes/+page.svelte
<script lang="ts">
import { trpc } from '$lib/trpc/client';
let greeting = 'press the button to load data';
let loading = false;
const loadData = async () => {
loading = true;
greeting = await trpc().greeting.query();
loading = false;
};
</script>
<h6>Loading data in<br /><code>+page.svelte</code></h6>
<a
href="#load"
role="button"
class="secondary"
aria-busy={loading}
on:click|preventDefault={loadData}>Load</a
>
<p>{greeting}</p>
All the related code to the websocket implementation is located at package/src/websocket
.
Exports a vite plugin that handles in dev mode the websocket lifecycle.
- On init:
configureServer
createWSSGlobalInstance
- Listen for
upgrade
events in vite dev server, so we can upgrade/trpc
to our tRPC server
On init we create a WebSocketServer
with the property noServer
so we can handle the upgrade to our tRPC and don't break the default vite websocket.
We store a reference in globalThis
to the web socket server, so we can later get a reference from SvelteKit side.
To store the websocket server without colliding with existing stuff in
globalThis
atsrc/websocket/svelteKitServer.ts
we create aSymbol
so we can reference the tRPC websocket like so:globalThis[Symbol.for('trpc.sveltekit.wss')]
Then we set up an event listener to the vite dev http server to handle the upgrade
event from onHttpServerUpgrade
. It will check that the path is /trpc
, if so it will upgrade our request to our tRPC websocket server.
Exports functions to handle the lifecycle of the tRPC websocket server:
createWSSGlobalInstance
onHttpServerUpgrade
The firsts 2 methods are already explained in the
vitePlugin.ts
section.
SvelteKitTRPCWSServer
The Vite plugin only works while the Vite dev server is running. When building for production we need to take a diferent aproach.
When we build a SvelteKit app, it will output a ./build
directory.
This function takes import.meta.url
as an argument from the root directory of the project (next to package.json
) and then converts it to __dirname
.
First it creates a websocket server attached to globalThis
, as explained, then imports dynamically from ${__dirname}/build
directory the index.js
file, that exports a server
property that contains an http server.
We attach to this server onHttpServerUpgrade
so we handle in the production server the tRPC websocket.
The function createTRPCWebSocketServer
handles the creation of the websocket tRPC handler getting the wss
from globalThis
.
This current implementation in case we are prerendering would fail as vite does not call configureServer
on the build step, so no wss
server is found in globalThis
.
This is why, when calling this method, we have to add a guard on the client/consumer code:
import { building } from '$app/environment';
if (!building) // 👈 Prevent from calling when building/prerendering
createTRPCWebSocketServer({ router, createContext })
createTRPCWebSocketClient
Creates the tRPC proxy client and links to the wss
.
Currently all the tRPC requests are handled via websockets, but this could be changed to only handle subscriptions.
Huge thanks to Alex / KATT, the author of tRPC, for being the first sponsor of this project! 🎉
On 24th of February 2022 Russia unlawfully invaded Ukraine. This is an unjustified, unprovoked attack on the sovereignty of a neighboring country, but also an open affront to international peace and stability that has the potential to degenerate into a nuclear event threatening the very existence of humanity. I am an EU (Romanian) citizen, but I am doing everything in my power to stop this madness. I stand with Ukraine. The entire Svelte community ❤️🇺🇦. Here's how you can show your support.
The ISC License.