trpc-sveltekit

SvelteKit adapter for trpc.io

Usage no npm install needed!

<script type="module">
  import trpcSveltekit from 'https://cdn.skypack.dev/trpc-sveltekit';
</script>

README

tRPC-SvelteKit

✨ tRPC-SvelteKit

NPM version License Downloads

End-to-end typesafe APIs with tRPC.io in SvelteKit applications.
No code generation, run-time bloat, or build pipeline.

Key features

✅ Works with @sveltejs/adapter-node & @sveltejs/adapter-vercel
✅ Works with SvelteKit's load() function for SSR

Example application with Prisma & superjson

👉 tRPC-Sveltekit-Example

TL;DR

Add this in your SvelteKit app hooks:

// src/hooks.ts
import { createTRPCHandle } from 'trpc-sveltekit';
// create your tRPC router...

export const handle = createTRPCHandle({ url: '/trpc', router }); // 👈 add this handle

How to use

  1. Install this package

npm install trpc-sveltekit/yarn add trpc-sveltekit

  1. Create your tRPC routes, context and type exports:
// $lib/trpcServer.ts
import type { inferAsyncReturnType } from '@trpc/server';
import * as trpc from '@trpc/server';

// optional
export const createContext = () => {
  // ...
  return {
    /** context data */
  };
};

// optional
export const responseMeta = () => {
  // ...
  return {
    // { headers: ... }
  };
};

export const router = trpc
  .router<inferAsyncReturnType<typeof createContext>>()
  // queries and mutations...
  .query('hello', {
    resolve: () => 'world',
  });

export type Router = typeof router;
  1. Add this handle to your application hooks (src/hooks.ts or src/hooks/index.ts):
// src/hooks.ts or src/hooks/index.ts
import { createContext, responseMeta, router } from '$lib/trpcServer';
import { createTRPCHandle } from 'trpc-sveltekit';

export const handle = createTRPCHandle({
  url: '/trpc', // optional; defaults to '/trpc'
  router,
  createContext, // optional
  reponseMeta, // optional
});

Learn more about SvelteKit hooks here.

  1. Create a tRPC client:
// $lib/trpcClient.ts
import type { Router } from '$lib/trpcServer'; // 👈 only the types are imported from the server
import * as trpc from '@trpc/client';

export default trpc.createTRPCClient<Router>({ url: '/trpc' });
  1. Use the client like so:
// page.svelte
import trpcClient from '$lib/trpcClient';

const greeting = await trpcClient.query('hello');
console.log(greeting); // => 👈 world

Recipes & caveats 🛠

Usage with Prisma

When you're building your SvelteKit app for production, you must instantiate your Prisma client like this: ✔️

// $lib/prismaClient.ts
import pkg from '@prisma/client';
const { PrismaClient } = pkg;

const prismaClient = new PrismaClient();
export default prismaClient;

This will not work: ❌

// $lib/prismaClient.ts
import { PrismaClient } from '@prisma/client';

const prismaClient = new PrismaClient();
export default prismaClient;

Configure superjson to correctly handle Decimal.js / Prisma.Decimal

❓ If you don't know why you'd want to use superjson, learn more about tRPC data transformers here.

By default, superjson only supports built-in data types to keep the bundle-size as small as possible. Here's how you could extend it with Decimal.js / Prisma.Decimal support:

// $lib/trpcTransformer.ts
import Decimal from 'decimal.js';
import superjson from 'superjson';

superjson.registerCustom<Decimal, string>(
  {
    isApplicable: (v): v is Decimal => Decimal.isDecimal(v),
    serialize: (v) => v.toJSON(),
    deserialize: (v) => new Decimal(v),
  },
  'decimal.js'
);

export default superjson;

Then, configure your tRPC router like so:

// $lib/trpcServer.ts
import trpcTransformer from '$lib/trcpTransformer';
import * as trpc from '@trpc/server';

export const router = trpc
  .router()
  // .merge, .query, .mutation, etc.
  .transformer(trpcTransformer); // 👈

export type Router = typeof router;

...and don't forget to configure your tRPC client:

// $lib/trpcClient.ts
import type { Router } from '$lib/trpcServer';
import transformer from '$lib/trpcTransformer';
import * as trpc from '@trpc/client';

export default trpc.createTRPCClient<Router>({
  url: '/trpc',
  transformer, // 👈
});

⚠️ You'll also have to use this custom svelte.config.js in order to be able to build your application for production with adapter-node/adapter-vercel:

// svelte.config.js
import adapter from '@sveltejs/adapter-node'; // or Vercel 
import preprocess from 'svelte-preprocess';

/** @type {import('@sveltejs/kit').Config} */
const config = {
  preprocess: preprocess(),

  kit: {
    adapter: adapter(),
    vite:
      process.env.NODE_ENV === 'production'
        ? {
            ssr: {
              noExternal: ['superjson'],
            },
          }
        : undefined,
  },
};

export default config;

Client-side helper types

It is often useful to wrap the functionality of your @trpc/client api within other functions. For this purpose, it's necessary to be able to infer input types, output types, and api paths generated by your @trpc/server router. Using tRPC's inference helpers, you could do something like:

// $lib/trpcClient.ts
import type { Router } from '$lib/trpcServer';
import trpcTransformer from '$lib/trpcTransformer';
import * as trpc from '@trpc/client';
import type { inferProcedureInput, inferProcedureOutput } from '@trpc/server';

export default trpc.createTRPCClient<Router>({
  url: '/trpc',
  transformer: trpcTransformer,
});

type Query = keyof Router['_def']['queries'];
type Mutation = keyof Router['_def']['mutations'];

// Useful types 👇👇👇
export type InferQueryOutput<RouteKey extends Query> = inferProcedureOutput<Router['_def']['queries'][RouteKey]>;
export type InferQueryInput<RouteKey extends Query> = inferProcedureInput<Router['_def']['queries'][RouteKey]>;
export type InferMutationOutput<RouteKey extends Mutation> = inferProcedureOutput<
  Router['_def']['mutations'][RouteKey]
>;
export type InferMutationInput<RouteKey extends Mutation> = inferProcedureInput<Router['_def']['mutations'][RouteKey]>;

Then, you could use the inferred types like so:

// authors.svelte
<script lang="ts">
  let authors: InferQueryOutput<'authors:browse'> = [];

  const loadAuthors = async () => {
    authors = await trpc.query('authors:browse', { genre: 'fantasy' });
  };
</script>

Server-Side Rendering

If you need to use the tRPC client in SvelteKit's load() function for SSR, make sure to initialize it like so:

// $lib/trpcClient.ts
import { browser } from '$app/env';
import type { Router } from '$lib/trpcServer';
import * as trpc from '@trpc/client';

const client = trpc.createTRPCClient<Router>({
  url: browser ? '/trpc' : 'http://localhost:3000/trpc', // 👈
});

Vercel's Edge Cache for Serverless Functions

Your server responses must satisfy some criteria in order for them to be cached Verced Edge Network, and here's where tRPC's responseMeta() comes in handy. You could initialize your handle in src/hooks.ts like so:

// src/hooks.ts or src/hooks/index.ts
import { router } from '$lib/trpcServer';
import { createTRPCHandle } from 'trpc-sveltekit';

export const handle = createTRPCHandle({
  url: '/trpc',
  router,
  responseMeta({ type, errors }) {
    if (type === 'query' && errors.length === 0) {
      const ONE_DAY_IN_SECONDS = 60 * 60 * 24;
      return {
        headers: {
          'cache-control': `s-maxage=1, stale-while-revalidate=${ONE_DAY_IN_SECONDS}`
        }
      };
    }
    return {};
  }
});

Example

See an example with Prisma & superjson: ✨

License

The ISC License.