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.
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:
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:
<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.