tRPC-SvelteKit v4.0.2

End-to-end typesafe APIs for your SvelteKit applications

Recipes and caveats

Client options

By default, createTRPCClient uses an 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:

createTRPCClient<Router>({
  init,
  headers: () => ({ authorization: getToken() }),
  maxURLLength: 2083
});

Custom links

If you need full control, pass your own links instead. For example, to stream responses with httpBatchStreamLink and use subscriptions over server-sent events, which work with every SvelteKit adapter:

import { httpBatchStreamLink, httpSubscriptionLink, splitLink } from '@trpc/client';

const url = `${location.origin}/trpc`;

createTRPCClient<Router>({
  links: [
    splitLink({
      condition: (op) => op.type === 'subscription',
      true: httpSubscriptionLink({ url }),
      false: httpBatchStreamLink({ url })
    })
  ]
});

Setting response headers and cookies

To change the headers of the response, you want to use the event provided in createContext. You may also 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.

lib/trpc/context.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<ReturnType<typeof createContext>>;

Using custom data transformers

The createTRPCClient method optionally accepts a transformer option, which it passes to its link (in tRPC v11, transformers are set on links rather than on the client). Please refer to this section in the tRPC.io documentation for more information, and keep in mind that you'll have to use the same transformer when you're defining the router and when you're creating the client.

If you're looking for a convenient transformer based on superjson with Decimal.js support, consider using the trpc-transformer package. Keep in mind that you'll have to install the superjson and decimal.js peer dependencies as well.

Response caching

Your server responses must satisfy some criteria in order for them to be cached (i.e. by Vercel's Edge Network). Please refer to this section of the tRPC.io documentation for more information.

The createTRPCHandle method accepts all the options of tRPC's HTTP handlers, including a responseMeta function.

Inferring types

It is often useful to wrap functionality of your tRPC client API within other functions. For this purpose, it's necessary to be able to infer input types and output types generated by your tRPC router. The @trpc/server package exports two helper types to assist with inferring these types from your router, namely inferRouterInputs and inferRouterOutputs. Please refer to this section of the tRPC.io documentation for more information.

To make your code faster to type and easier to read, you could further refine these helper types when defining your router:

lib/trpc/router.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<Router>;
export type RouterOutputs = inferRouterOutputs<Router>;

Then, you could use the helper types in your pages code like so:

routes/authors/+page.svelte
<script lang="ts">
  import { refreshAll } from '$app/navigation';
  import AuthorizationAlert from '#lib/components/AuthorizationAlert.svelte';
  import DataTable from '#lib/components/DataTable.svelte';
  import ModalEditor from '#lib/components/ModalEditor.svelte';
  import TextInput from '#lib/components/inputs/TextInput.svelte';
  import TextareaInput from '#lib/components/inputs/TextareaInput.svelte';
  import { trpc } from '#lib/trpc/client.ts';
  import type { RouterInputs } from '#lib/trpc/router.ts';
  import { TRPCClientError } from '@trpc/client';

  let { data } = $props();

  let busy = $state(false);
  let item: RouterInputs['authors']['save'] | null = $state(null); // 👈 we're using a helper type
  let errors: { message: string; path: string[] }[] | null = $state(null);
  let needsAuthorization = $state(false);

  const handleAdd = async () => {
    if (!data.isAuthenticated) {
      needsAuthorization = true;
      return;
    }

    item = { id: null, firstName: '', lastName: '', bio: '' };
  };

  const handleEdit = async (id: string) => {
    if (!data.isAuthenticated) {
      needsAuthorization = true;
      return;
    }

    busy = true;
    item = await trpc().authors.load.query(id);
    busy = false;
  };

  const handleDelete = async (id: string) => {
    if (!data.isAuthenticated) {
      needsAuthorization = true;
      return;
    }

    busy = true;
    await trpc().authors.delete.mutate(id);
    await refreshAll();
    busy = false;
  };

  const handleCancel = () => {
    item = null;
    errors = null;
  };

  const handleSave = async (input: RouterInputs['authors']['save']) => {
    if (!data.isAuthenticated) {
      needsAuthorization = true;
      return;
    }

    busy = true;
    try {
      await trpc().authors.save.mutate(input);
      item = null;
      errors = null;
      await refreshAll();
    } catch (err) {
      if (err instanceof TRPCClientError) {
        errors = JSON.parse(err.message);
      } else {
        throw err;
      }
    } finally {
      busy = false;
    }
  };
</script>

<svelte:head>
  <title>Authors • Bookstall</title>
</svelte:head>

<DataTable
  {busy}
  title="Authors"
  items={data.authors}
  columns={[
    {
      title: 'Name',
      grow: true,
      accessor: ({ firstName, lastName }) => `${firstName} ${lastName}`
    },
    {
      title: 'Books',
      align: 'right',
      accessor: (author) => author._count.books
    }
  ]}
  onadd={handleAdd}
  onedit={handleEdit}
  ondelete={handleDelete}
/>

<ModalEditor {item} itemName="author" oncancel={handleCancel} onsave={handleSave}>
  <div class="grid">
    <TextInput name="firstName" label="First name" required {errors} {item} />
    <TextInput name="lastName" label="Last name" required {errors} {item} />
  </div>
  <TextareaInput name="bio" label="Bio" {errors} {item} />
</ModalEditor>

<AuthorizationAlert visible={needsAuthorization} onclose={() => (needsAuthorization = false)} />

Reserved path prefix

Invoking the createTRPCHandle method reserves the /trpc path prefix for the tRPC API. This means that you cannot use this prefix for any other purpose. If you need to use this prefix for other purposes, you can use the url option to change the reserved prefix.

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.