Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://icflorescu.github.io/trpc-sveltekit/

End-to-end typesafe APIs with tRPC.io for your SvelteKit applications.
https://icflorescu.github.io/trpc-sveltekit/

svelte sveltekit sveltekit-adapter trpc typescript

Last synced: about 1 month ago
JSON representation

End-to-end typesafe APIs with tRPC.io for your SvelteKit applications.

Awesome Lists containing this project

README

        

# tRPC-SvelteKit

![Publish NPM & deploy docs workflow](https://github.com/icflorescu/trpc-sveltekit/actions/workflows/publish-and-deploy.yml/badge.svg)
[![NPM version][npm-image]][npm-url]
[![License][license-image]][license-url]
[![Stars][stars-image]][stars-url]
[![Last commit][last-commit-image]][repo-url]
[![Closed issues][closed-issues-image]][closed-issues-url]
[![Downloads][downloads-image]][npm-url]
[![Language][language-image]][repo-url]
[![Sponsor the author][sponsor-image]][sponsor-url]

Documentation available at [icflorescu.github.io/trpc-sveltekit](https://icflorescu.github.io/trpc-sveltekit/).

[![tRPC-SvelteKit](https://user-images.githubusercontent.com/581999/204399415-18fddfb9-acdf-4e15-a945-a27f816e354e.png)](https://icflorescu.github.io/trpc-sveltekit/)

> Move fast and break nothing.
> End-to-end typesafe APIs for your
> SvelteKit applications.

## Works with

βœ… `@sveltejs/adapter-node`
βœ… `@sveltejs/adapter-vercel`
βœ… `@sveltejs/adapter-netlify`

## Important

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](https://github.com/icflorescu/trpc-sveltekit/tree/trpc-v9) branch.

## Quickstart

### Install the package and its dependencies:

```bash
yarn add trpc-sveltekit @trpc/server @trpc/client
```

### Create your [tRPC router](https://trpc.io/docs/router):

```ts
// 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().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](https://trpc.io/docs/context):

```ts
// 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;
```

### Add this handle to your SvelteKit app [hooks](https://kit.svelte.dev/docs/hooks):

```ts
// 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 });
```

### Define a helper function to easily use the tRPC client in your pages:

```ts
// lib/trpc/client.ts
import type { Router } from '$lib/trpc/router';
import { createTRPCClient, type TRPCClientInit } from 'trpc-sveltekit';

let browserClient: ReturnType>;

export function trpc(init?: TRPCClientInit) {
const isBrowser = typeof window !== 'undefined';
if (isBrowser && browserClient) return browserClient;
const client = createTRPCClient({ init });
if (isBrowser) browserClient = client;
return client;
}
```

### Call the tRPC procedures in your pages:

```ts
// routes/+page.svelte

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;
};

Loading data in
+page.svelte

Load

{greeting}


```
## Examples

This repository contains a handful of examples:

- [simple](https://github.com/icflorescu/trpc-sveltekit/tree/main/examples/simple)
- [bookstall](https://github.com/icflorescu/trpc-sveltekit/tree/main/examples/bookstall)
- [websocket](https://github.com/icflorescu/trpc-sveltekit/tree/main/examples/websocket)

---

## EXPERIMENTAL WebSocket support
(courtesy of [@SrZorro](https://github.com/SrZorro))

SvelteKit [doesn't (yet) offer WebSockets support](https://github.com/sveltejs/kit/issues/1491), 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](#websockets-implementation-details) to find out how this works under the hood).

### Caveats

- Works with [@sveltejs/adapter-node](https://www.npmjs.com/package/@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.

### Install the package and its dependencies:

```bash
yarn add trpc-sveltekit @trpc/server @trpc/client @sveltejs/adapter-node ws
```

### Setup workarounds

In your `vite.config.ts`, add:

```ts
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:

```ts
import adapter from '@sveltejs/adapter-node'; // βž•
// import adapter from '@sveltejs/adapter-auto'; // βž–
```

Create this file next to `package.json` your server entrypoint:

```js
// wsServer.js
import { SvelteKitTRPCWSServer } from "trpc-sveltekit/websocket";

SvelteKitTRPCWSServer(import.meta.url);
```

In your `package.json` `scripts`, modify the `start` command:

```json
{
"scripts": {
"start": "node ./wsServer"
}
}
```

### Create your tRPC router & context

- [Create your tRPC router](#create-your-trpc-router)
- [Create a tRPC context](#create-a-trpc-context)

### Call this function from your SvelteKit app [server hooks](https://kit.svelte.dev/docs/hooks#server-hooks):

```ts
// 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 })
```

### Define a helper function to easily use the tRPC client in your pages:

```ts
// lib/trpc/client.ts
import type { Router } from '$lib/trpc/router';
import { createTRPCWebSocketClient } from "trpc-sveltekit/websocket";

let browserClient: ReturnType>;

export function trpc() {
const isBrowser = typeof window !== 'undefined';
if (isBrowser && browserClient) return browserClient;
const client = createTRPCWebSocketClient();
if (isBrowser) browserClient = client;
return client;
}
```

### Call the tRPC procedures in your pages:

```ts
// routes/+page.svelte

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;
};

Loading data in
+page.svelte

Load

{greeting}


```

### Implementation details

All the related code to the websocket implementation is located at `package/src/websocket`.

#### `vitePlugin.ts`

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` at `src/websocket/svelteKitServer.ts` we create a `Symbol`
> 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.

#### `svelteKitServer.ts`

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.

#### `server.ts`

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:

```ts
import { building } from '$app/environment';

if (!building) // πŸ‘ˆ Prevent from calling when building/prerendering
createTRPCWebSocketServer({ router, createContext })
```

#### `client.ts`

`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](https://trpc.io/docs/links).

---

## Contributors

[![Contributors list](https://contrib.rocks/image?repo=icflorescu/trpc-sveltekit)](https://github.com/icflorescu/trpc-sveltekit/graphs/contributors)

## Acknowledgements

Huge thanks to [Alex / KATT](https://github.com/KATT), the author of [tRPC](https://trpc.io/), for being the first sponsor of this project! πŸŽ‰

## Stand with Ukraine

On 24th of February 2022 [Russia unlawfully invaded Ukraine](https://en.wikipedia.org/wiki/Russo-Ukrainian_War). 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](https://www.stopputin.net/).

## License

The [ISC License](https://github.com/icflorescu/trpc-sveltekit/blob/master/LICENSE).

[npm-url]: https://npmjs.org/package/trpc-sveltekit
[repo-url]: https://github.com/icflorescu/trpc-sveltekit
[stars-url]: https://github.com/icflorescu/trpc-sveltekit/stargazers
[closed-issues-url]: https://github.com/icflorescu/trpc-sveltekit/issues?q=is%3Aissue+is%3Aclosed
[license-url]: LICENSE
[npm-image]: https://img.shields.io/npm/v/trpc-sveltekit.svg?style=flat-square
[license-image]: http://img.shields.io/npm/l/trpc-sveltekit.svg?style=flat-square
[downloads-image]: http://img.shields.io/npm/dm/trpc-sveltekit.svg?style=flat-square
[stars-image]: https://img.shields.io/github/stars/icflorescu/trpc-sveltekit?style=flat-square
[last-commit-image]: https://img.shields.io/github/last-commit/icflorescu/trpc-sveltekit?style=flat-square
[closed-issues-image]: https://img.shields.io/github/issues-closed-raw/icflorescu/trpc-sveltekit?style=flat-square
[language-image]: https://img.shields.io/github/languages/top/icflorescu/trpc-sveltekit?style=flat-square
[sponsor-image]: https://img.shields.io/badge/sponsor-violet?style=flat-square
[sponsor-url]: https://github.com/sponsors/icflorescu