<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>tRPC Blog</title>
        <link>https://trpc.io/blog</link>
        <description>tRPC Blog</description>
        <lastBuildDate>Fri, 21 Mar 2025 00:00:00 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <language>en</language>
        <item>
            <title><![CDATA[Announcing tRPC v11]]></title>
            <link>https://trpc.io/blog/announcing-trpc-v11</link>
            <guid>https://trpc.io/blog/announcing-trpc-v11</guid>
            <pubDate>Fri, 21 Mar 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[Although tRPC v11 has been production-ready for a long time via the @next tag, we've gotten kinda addicted to adding new features without being sticklers to semantic versioning. Today, we're excited to finally be ripping off the band-aid and announcing the official release of tRPC v11!]]></description>
            <content:encoded><![CDATA[<p>Although tRPC v11 has been production-ready for a long time via the <code>@next</code> tag, we've gotten kinda addicted to adding new features without being sticklers to semantic versioning. Today, we're excited to <em>finally</em> be ripping off the band-aid and announcing the official release of tRPC v11!</p>
<!-- -->
<p>Since our last major version release in November 2022, the tRPC community has seen substantial growth:</p>
<ul>
<li>We now have over <a href="https://github.com/trpc/trpc" target="_blank" rel="noopener noreferrer">35,000 stars on GitHub</a></li>
<li><a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">A Discord community</a> with over 5,000 members</li>
<li><a href="https://www.npmjs.com/package/@trpc/server" target="_blank" rel="noopener noreferrer">700k+ weekly npm downloads</a></li>
<li><a href="https://github.com/trpc/trpc/graphs/contributors" target="_blank" rel="noopener noreferrer">100s of contributors</a></li>
<li><a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">An <em>awesome</em> ecosystem of extensions, examples, and content</a></li>
</ul>
<p>With the launch of tRPC v11, we're excited to share that v11 is already being used in production by many large TypeScript projects thanks to its stable evolution on our <code>@next</code> channel</p>
<p>For new projects, you can get up and running with an <a href="https://trpc.io/awesome#-starting-points-example-projects-etc" target="_blank" rel="noopener noreferrer">example application</a> to learn about tRPC v11. For projects still on tRPC v10, <a href="https://trpc.io/docs/v11/migrate-from-v10-to-v11" target="_blank" rel="noopener noreferrer">visit the v11 migration guide</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="overview-of-changes">Overview of changes<a href="https://trpc.io/blog/announcing-trpc-v11#overview-of-changes" class="hash-link" aria-label="Direct link to Overview of changes" title="Direct link to Overview of changes">​</a></h2>
<p>v11 is a largely backward-compatible release with v10, but it brings a lot of new features and improvements. Here are the highlights:</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="tanstack-query-v5-support">TanStack Query v5 support<a href="https://trpc.io/blog/announcing-trpc-v11#tanstack-query-v5-support" class="hash-link" aria-label="Direct link to TanStack Query v5 support" title="Direct link to TanStack Query v5 support">​</a></h3>
<p>When <a href="https://tanstack.com/blog/announcing-tanstack-query-v5" target="_blank" rel="noopener noreferrer">TanStack Query v5</a> was released it necessitated some breaking changes within tRPC's react-query integration. This has been available since quite early on via the <code>@next</code> tag, but now it's officially released. Many projects have already chosen to upgrade and are enjoying the benefits of full React Suspense support and many other improvements, for advice on migrating your tRPC React client code you can follow <a href="https://tanstack.com/query/v5/docs/react/guides/migrating-to-v5" target="_blank" rel="noopener noreferrer">TanStack Query's own migration guide</a>.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="new-tanstack-react-query-integration">New TanStack React Query integration<a href="https://trpc.io/blog/announcing-trpc-v11#new-tanstack-react-query-integration" class="hash-link" aria-label="Direct link to New TanStack React Query integration" title="Direct link to New TanStack React Query integration">​</a></h3>
<p><a href="https://trpc.io/blog/introducing-tanstack-react-query-client">See the blog post</a></p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="formdata--non-json-content-types-support">FormData / Non-JSON Content Types support<a href="https://trpc.io/blog/announcing-trpc-v11#formdata--non-json-content-types-support" class="hash-link" aria-label="Direct link to FormData / Non-JSON Content Types support" title="Direct link to FormData / Non-JSON Content Types support">​</a></h3>
<p>One of our most requested features is the ability to send and receive more than simply JSON data. With tRPC v11, you can now send and receive data in a variety of <a href="https://trpc.io/docs/server/non-json-content-types">non-json content types</a>, <code>FormData</code>, and binary types such as <code>Blob</code>, <code>File</code> and <code>Uint8Array</code>. You can find an example of how to use these content types <a href="https://github.com/trpc/trpc/tree/next/examples/minimal-content-types" target="_blank" rel="noopener noreferrer">here</a></p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/http'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) router<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>(_: {
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>
import router">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) formData: MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">formData</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodCustom<FormData, FormData>>(schema: z.ZodCustom<FormData, FormData>): ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(alias) instanceof<{
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}>(cls: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}, params?: {
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
    message?: string | undefined | undefined;
}): z.ZodCustom<FormData, FormData>
export instanceof">instanceof</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="var FormData: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}">FormData</data-lsp>))</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, FormData, UnsetMarker, void>): MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) input: FormData" style="border-bottom: solid 2px lightgrey;">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: FormData</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) file: MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">file</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<UtilityParser<OctetInput, ReadableStream<any>>>(schema: UtilityParser<OctetInput, ReadableStream<any>>): ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp>)</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, ReadableStream<any>, UnsetMarker, void>): MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) input: ReadableStream<any>" style="border-bottom: solid 2px lightgrey;">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: ReadableStream&lt;any&gt;</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>, <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/http'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) router<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>(_: {
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    formData: MutationProcedure<{
        input: FormData;
        output: void;
        meta: object;
    }>;
    file: MutationProcedure<{
        input: OctetInput;
        output: void;
        meta: object;
    }>;
}>>
import router">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) formData: MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">formData</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodCustom<FormData, FormData>>(schema: z.ZodCustom<FormData, FormData>): ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(alias) instanceof<{
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}>(cls: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}, params?: {
    when?: ((payload: z.core.ParsePayload) => boolean) | undefined | undefined;
    error?: string | z.core.$ZodErrorMap<z.core.$ZodIssueCustom> | undefined;
    message?: string | undefined | undefined;
}): z.ZodCustom<FormData, FormData>
export instanceof">instanceof</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="var FormData: {
    new (form?: HTMLFormElement, submitter?: HTMLElement | null): FormData;
    prototype: FormData;
}">FormData</data-lsp>))</span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, FormData, FormData, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, FormData, UnsetMarker, void>): MutationProcedure<{
    input: FormData;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: FormData" style="border-bottom: solid 2px lightgrey;">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: FormData</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) file: MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">file</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<UtilityParser<OctetInput, ReadableStream<any>>>(schema: UtilityParser<OctetInput, ReadableStream<any>>): ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(alias) const octetInputParser: UtilityParser<OctetInput, ReadableStream<any>>
import octetInputParser">octetInputParser</data-lsp>)</span></div><div class="line"><span style="color: #C9D1D9">    .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, OctetInput, ReadableStream<any>, UnsetMarker, UnsetMarker, false>.mutation<void>(resolver: ProcedureResolver<object, object, object, ReadableStream<any>, UnsetMarker, void>): MutationProcedure<{
    input: OctetInput;
    output: void;
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: ReadableStream<any>" style="border-bottom: solid 2px lightgrey;">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="meta-line"><span class="popover-prefix">                        </span><span class="popover"><div class="arrow"></div>(parameter) input: ReadableStream&lt;any&gt;</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="react-server-components--nextjs-app-router">React Server Components / Next.js App Router<a href="https://trpc.io/blog/announcing-trpc-v11#react-server-components--nextjs-app-router" class="hash-link" aria-label="Direct link to React Server Components / Next.js App Router" title="Direct link to React Server Components / Next.js App Router">​</a></h3>
<p>While you have been able to use tRPC with Next.js App Router from day 1, by either:</p>
<ul>
<li>using a server-centric approach using async/await with either <a href="https://trpc.io/docs/server/server-side-calls"><code>createCaller</code></a> or <a href="https://trpc.io/docs/client/vanilla"><code>createTRPCClient</code></a>,</li>
<li>or a client-centric approach using the <a href="https://trpc.io/docs/client/react">React Query integration</a> and client-side hooks,</li>
</ul>
<p>The bridge in between has been a bit rough around the edges. Revalidation patterns are mixed when having to maintain different data fetching patterns, resulting in a less than ideal developer experience which does not live up to tRPC's standards.</p>
<p>To fix this, we've improved support for React Server Components (RSC) and added prefetch helpers to make it easier to utilize the power of RSCs running exclusively on the server, in combination with the highly dynamic client-side cache of React Query.
You can now start executing a procedure on the server in a RSC, pick up the pending promise on the client, and automatically hydrate the React Query cache clientside. This enables you to build highly dynamic applications without suffering from server-client waterfalls. You can read more in our <a href="https://trpc.io/docs/client/react/server-components">Server Components documentation</a>.</p>
<p>In addition to these new prefetch patterns, we've added experimental support for server functions. You can read more about this in <a href="https://trpc.io/blog/trpc-actions">our blog post</a>. We plan to keep iterating on this feature as server functions become a more established pattern in the ecosystem.</p>
<p>We have also worked with the <a href="https://x.com/tannerlinsley/status/1844500352655335522" target="_blank" rel="noopener noreferrer">TanStack team to help design the APIs for their server functions</a>. Our goal is to split out parts of tRPC's powerful middleware system into a separate package which can be used throughout the ecosystem.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="streaming-responses-from-queries-and-mutations">Streaming Responses from Queries and Mutations<a href="https://trpc.io/blog/announcing-trpc-v11#streaming-responses-from-queries-and-mutations" class="hash-link" aria-label="Direct link to Streaming Responses from Queries and Mutations" title="Direct link to Streaming Responses from Queries and Mutations">​</a></h3>
<p>We've introduced <a href="https://trpc.io/docs/client/links/httpBatchStreamLink">httpBatchStreamLink</a>, which allows you to stream responses from queries. This is helpful when working with large datasets or where you need to process data in real-time and dispatch to the frontend as you go. This is not a replacement for subscriptions, but is another option in your toolbelt.</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #C2C3C5">// @filename: server.ts</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) router<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>(_: {
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>
import router">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) examples: {
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}">examples</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) iterable: QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">iterable</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<AsyncGenerator<number, never, unknown>>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, AsyncGenerator<number, never, unknown>>): QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function*</span><span style="color: #24292EFF"> () {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let i: number">i</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">0</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">while</span><span style="color: #24292EFF"> (</span><span style="color: #1976D2">true</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>">Promise</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout (+2 overloads)
namespace setTimeout">setTimeout</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">500</span><span style="color: #24292EFF">));</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">yield</span><span style="color: #24292EFF"> <data-lsp lsp="let i: number">i</data-lsp></span><span style="color: #D32F2F">++</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      }</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>">AppRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> <data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp>;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #C2C3C5">// @filename: client.ts</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter[&quot;_def&quot;][&quot;_config&quot;][&quot;$types&quot;]>): TRPCLink<TRouter>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/client'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>): TRPCClient<...>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp></span><span style="color: #24292EFF">&gt;({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>[]">links</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> [</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) httpBatchStreamLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) url: string | URL">url</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'http://localhost:3000'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  ]</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>" style="border-bottom: solid 2px lightgrey;">iterable</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) examples: DecoratedProcedureRecord<{
    transformer: false;
    errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}>>">examples</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) iterable: {
    query: Resolver<{
        input: void;
        output: AsyncIterable<number, never, unknown>;
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}">iterable</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) query: (input: void, opts?: TRPCProcedureOptions) => Promise<AsyncIterable<number, never, unknown>>">query</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="meta-line"><span class="popover-prefix">         </span><span class="popover"><div class="arrow"></div>const iterable: AsyncIterable&lt;number, never, unknown&gt;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">for</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const value: number">value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">of</span><span style="color: #24292EFF"> <data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>">iterable</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2"><data-lsp lsp="namespace console
var console: Console">console</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">'Iterable:'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="const value: number" style="border-bottom: solid 2px lightgrey;">value</data-lsp>);</span></div><div class="meta-line"><span class="popover-prefix">                            </span><span class="popover"><div class="arrow"></div>const value: number</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #8B949E">// @filename: server.ts</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>, <data-lsp lsp="(alias) const router: RouterBuilder<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>
import router">router</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) router<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>(_: {
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}): BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>
import router">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) examples: {
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}">examples</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) iterable: QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">iterable</data-lsp>: <data-lsp lsp="(alias) const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>
import publicProcedure">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<AsyncGenerator<number, never, unknown>>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, AsyncGenerator<number, never, unknown>>): QueryProcedure<{
    input: void;
    output: AsyncGenerator<number, never, unknown>;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function*</span><span style="color: #C9D1D9"> () {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let i: number">i</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">0</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">while</span><span style="color: #C9D1D9"> (</span><span style="color: #79C0FF">true</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Promise: PromiseConstructor
new <unknown>(executor: (resolve: (value: unknown) => void, reject: (reason?: any) => void) => void) => Promise<unknown>">Promise</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="function setTimeout(callback: (_: void) => void, delay?: number): NodeJS.Timeout (+2 overloads)
namespace setTimeout">setTimeout</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) resolve: (value: unknown) => void">resolve</data-lsp>, </span><span style="color: #79C0FF">500</span><span style="color: #C9D1D9">));</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">yield</span><span style="color: #C9D1D9"> <data-lsp lsp="let i: number">i</data-lsp></span><span style="color: #FF7B72">++</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      }</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>">AppRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> <data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>">appRouter</data-lsp>;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #8B949E">// @filename: client.ts</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function createTRPCClient<TRouter extends AnyRouter>(opts: CreateTRPCClientOptions<TRouter>): TRPCClient<TRouter>
import createTRPCClient">createTRPCClient</data-lsp>, <data-lsp lsp="(alias) function httpBatchStreamLink<TRouter extends AnyRouter>(opts: HTTPBatchLinkOptions<TRouter[&quot;_def&quot;][&quot;_config&quot;][&quot;$types&quot;]>): TRPCLink<TRouter>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/client'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) createTRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: CreateTRPCClientOptions<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>): TRPCClient<...>
import createTRPCClient">createTRPCClient</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(alias) type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>> &amp; DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>
import AppRouter">AppRouter</data-lsp></span><span style="color: #C9D1D9">&gt;({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) links: TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>[]">links</data-lsp>: [</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) httpBatchStreamLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>(opts: HTTPBatchLinkOptions<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}>): TRPCLink<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<...>>>
import httpBatchStreamLink">httpBatchStreamLink</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) url: string | URL">url</data-lsp>: </span><span style="color: #A5D6FF">'http://localhost:3000'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  ],</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>" style="border-bottom: solid 2px lightgrey;">iterable</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> <data-lsp lsp="const trpc: TRPCClient<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    examples: {
        iterable: QueryProcedure<{
            input: void;
            output: AsyncGenerator<number, never, unknown>;
            meta: object;
        }>;
    };
}>>>">trpc</data-lsp>.<data-lsp lsp="(property) examples: DecoratedProcedureRecord<{
    transformer: false;
    errorShape: DefaultErrorShape;
}, DecorateCreateRouterOptions<{
    iterable: QueryProcedure<{
        input: void;
        output: AsyncGenerator<number, never, unknown>;
        meta: object;
    }>;
}>>">examples</data-lsp>.<data-lsp lsp="(property) iterable: {
    query: Resolver<{
        input: void;
        output: AsyncIterable<number, never, unknown>;
        errorShape: DefaultErrorShape;
        transformer: false;
    }>;
}">iterable</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) query: (input: void, opts?: TRPCProcedureOptions) => Promise<AsyncIterable<number, never, unknown>>">query</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="meta-line"><span class="popover-prefix">         </span><span class="popover"><div class="arrow"></div>const iterable: AsyncIterable&lt;number, never, unknown&gt;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">for</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const value: number">value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">of</span><span style="color: #C9D1D9"> <data-lsp lsp="const iterable: AsyncIterable<number, never, unknown>">iterable</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="namespace console
var console: Console">console</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'Iterable:'</span><span style="color: #C9D1D9">, <data-lsp lsp="const value: number" style="border-bottom: solid 2px lightgrey;">value</data-lsp>);</span></div><div class="meta-line"><span class="popover-prefix">                            </span><span class="popover"><div class="arrow"></div>const value: number</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="shorthand-router-definitions">Shorthand Router Definitions<a href="https://trpc.io/blog/announcing-trpc-v11#shorthand-router-definitions" class="hash-link" aria-label="Direct link to Shorthand Router Definitions" title="Direct link to Shorthand Router Definitions">​</a></h3>
<p>We've introduced a new shorthand syntax for defining your router to simplify the process of defining your routes. <a href="https://trpc.io/docs/server/routers#inline-sub-router">Docs</a></p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>(_: {
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// Shorthand plain object for creating a sub-router</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) nested1: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}">nested1</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(() </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'...'</span><span style="color: #24292EFF">)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// Equivalent of:</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) nested2: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">nested2</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>(_: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(() </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'...'</span><span style="color: #24292EFF">)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}>(_: {
    nested1: {
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    };
    nested2: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        proc: QueryProcedure<{
            input: void;
            output: string;
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// Shorthand plain object for creating a sub-router</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) nested1: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}">nested1</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(() </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'...'</span><span style="color: #C9D1D9">),</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// Equivalent of:</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) nested2: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">nested2</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>(_: {
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}) => BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    proc: QueryProcedure<{
        input: void;
        output: string;
        meta: object;
    }>;
}>>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) proc: QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">proc</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.query<string>(resolver: ProcedureResolver<object, object, object, UnsetMarker, UnsetMarker, string>): QueryProcedure<{
    input: void;
    output: string;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(() </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'...'</span><span style="color: #C9D1D9">),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="subscriptions-server-sent-events-and-other-improvements">Subscriptions: Server-Sent Events and other Improvements<a href="https://trpc.io/blog/announcing-trpc-v11#subscriptions-server-sent-events-and-other-improvements" class="hash-link" aria-label="Direct link to Subscriptions: Server-Sent Events and other Improvements" title="Direct link to Subscriptions: Server-Sent Events and other Improvements">​</a></h3>
<ul>
<li>tRPC v11 introduces a new way to handle subscriptions using <a href="https://trpc.io/docs/server/subscriptions#websockets-or-server-sent-events">Server-Sent Events (SSE)</a>. This is a great way to handle real-time updates in your application without the need for more complex WebSockets. We recommend using this in the first instance going forward.</li>
<li>We've added support for JavaScript <a href="https://trpc.io/docs/server/subscriptions#cleanup-of-side-effects">generators in subscriptions</a>. This allows you to write more complex subscription handlers which can yield multiple values over time, and clean up when finished, in a very JS-native way.</li>
<li>Subscriptions now support <a href="https://trpc.io/docs/server/subscriptions#output-validation">Output validators</a>, improving the type-safety of your subscription handlers.</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="goodbye-v9-interop-mode">Goodbye v9 <code>.interop()</code>-mode<a href="https://trpc.io/blog/announcing-trpc-v11#goodbye-v9-interop-mode" class="hash-link" aria-label="Direct link to goodbye-v9-interop-mode" title="Direct link to goodbye-v9-interop-mode">​</a></h3>
<p>In tRPC v10, we introduced the <code>.interop()</code>-mode to provide a smooth migration path for tRPC v9 users. With tRPC v11, we've removed the <code>.interop()</code>-mode. If you're still using the <code>.interop()</code>-mode, you can use the <a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">v10 migration guide</a> to complete your transition to the APIs used by tRPC today.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="migrating-to-v11">Migrating to v11<a href="https://trpc.io/blog/announcing-trpc-v11#migrating-to-v11" class="hash-link" aria-label="Direct link to Migrating to v11" title="Direct link to Migrating to v11">​</a></h2>
<p>If you're currently using tRPC v10, you can follow the <a href="https://trpc.io/docs/migrate-from-v10-to-v11">migration guide</a> to upgrade to v11. The migration guide covers all the breaking changes and new features in v11.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="thank-you">Thank you!<a href="https://trpc.io/blog/announcing-trpc-v11#thank-you" class="hash-link" aria-label="Direct link to Thank you!" title="Direct link to Thank you!">​</a></h2>
<p>From the whole tRPC Core Team, thank you for using and supporting tRPC.</p>
<hr>
<ul>
<li>Follow <a href="https://twitter.com/trpcio" target="_blank" rel="noopener noreferrer">@trpcio</a> on Twitter.</li>
<li>Join our <a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord-community</a></li>
<li><a href="https://trpc.io/#try-it-out" target="_blank" rel="noopener noreferrer">Try out tRPC in your browser</a></li>
</ul>
<a id="sponsor-button" href="https://trpc.io/sponsor" class="group flex h-12 w-max items-center gap-4 rounded-lg border-2 bg-zinc-200 px-4 py-2 transition hover:bg-zinc-100 dark:border-zinc-900 dark:bg-zinc-800 hover:dark:border-zinc-700 hover:dark:bg-zinc-900"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="aspect-square h-6 fill-pink-500 transition-transform duration-200 ease-in group-hover:scale-110"><path d="M17.625 1.499c-2.32 0-4.354 1.203-5.625 3.03-1.271-1.827-3.305-3.03-5.625-3.03C3.129 1.499 0 4.253 0 8.249c0 4.275 3.068 7.847 5.828 10.227a33.14 33.14 0 0 0 5.616 3.876l.028.017.008.003-.001.003c.163.085.342.126.521.125.179.001.358-.041.521-.125l-.001-.003.008-.003.028-.017a33.14 33.14 0 0 0 5.616-3.876C20.932 16.096 24 12.524 24 8.249c0-3.996-3.129-6.75-6.375-6.75zm-.919 15.275a30.766 30.766 0 0 1-4.703 3.316l-.004-.002-.004.002a30.955 30.955 0 0 1-4.703-3.316c-2.677-2.307-5.047-5.298-5.047-8.523 0-2.754 2.121-4.5 4.125-4.5 2.06 0 3.914 1.479 4.544 3.684.143.495.596.797 1.086.796.49.001.943-.302 1.085-.796.63-2.205 2.484-3.684 4.544-3.684 2.004 0 4.125 1.746 4.125 4.5 0 3.225-2.37 6.216-5.048 8.523z"></path></svg><span class="font-semibold text-zinc-900 no-underline dark:text-zinc-300">Become a Sponsor!</span></a>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing the new TanStack React Query integration]]></title>
            <link>https://trpc.io/blog/introducing-tanstack-react-query-client</link>
            <guid>https://trpc.io/blog/introducing-tanstack-react-query-client</guid>
            <pubDate>Mon, 17 Feb 2025 00:00:00 GMT</pubDate>
            <description><![CDATA[We are excited to announce the new TanStack React Query integration for tRPC is now available on tRPC's next-release. Compared to our classic React Query Integration it's simpler and more TanStack Query-native, choosing to utilize the QueryOptions and MutationOptions interfaces native to TanStack React Query, instead of wrapping useQuery and useMutation with our own client.]]></description>
            <content:encoded><![CDATA[<p>We are excited to announce the new TanStack React Query integration for tRPC is now available on tRPC's <code>next</code>-release. Compared to our classic <a href="https://trpc.io/docs/client/react">React Query Integration</a> it's simpler and more TanStack Query-native, choosing to utilize the <a href="https://tanstack.com/query/latest/docs/framework/react/guides/query-options" target="_blank" rel="noopener noreferrer">QueryOptions</a> and <a href="https://tanstack.com/query/latest/docs/framework/react/guides/mutations" target="_blank" rel="noopener noreferrer">MutationOptions</a> interfaces native to TanStack React Query, instead of wrapping <code>useQuery</code> and <code>useMutation</code> with our own client.</p>
<!-- -->
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="greeting.tsx"><div class="code-title">greeting.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<NoInfer<TData>, TError> (+2 overloads)
import useQuery">useQuery</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@tanstack/react-query'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function Greeting(): null">Greeting</data-lsp></span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const greetingQuery: UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>">greetingQuery</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) useQuery<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>> (+2 overloads)
import useQuery">useQuery</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) greeting: DecorateQueryProcedure<{
    input: {
        name: string;
    };
    output: string;
    transformer: false;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> &amp; Record<string, never>">greeting</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) DecorateQueryProcedure<{ input: { name: string; }; output: string; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<string, string>(input: {
    name: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> (+2 overloads)">queryOptions</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) name: string">name</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Jerry'</span><span style="color: #24292EFF"> }));</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// greetingQuery.data === 'Hello Jerry'</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">null</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="greeting.tsx"><div class="code-title">greeting.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function useQuery<TQueryFnData = unknown, TError = Error, TData = TQueryFnData, TQueryKey extends QueryKey = readonly unknown[]>(options: DefinedInitialDataOptions<TQueryFnData, TError, TData, TQueryKey>, queryClient?: QueryClient): DefinedUseQueryResult<NoInfer<TData>, TError> (+2 overloads)
import useQuery">useQuery</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@tanstack/react-query'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const useTRPC: () => TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function Greeting(): null">Greeting</data-lsp></span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) useTRPC(): TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>
import useTRPC">useTRPC</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const greetingQuery: UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>>">greetingQuery</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) useQuery<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>(options: UndefinedInitialDataOptions<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, string, TRPCQueryKeyWithoutPrefix>, queryClient?: QueryClient): UseQueryResult<string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>> (+2 overloads)
import useQuery">useQuery</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const trpc: TRPCOptionsProxy<BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    greeting: QueryProcedure<{
        input: {
            name: string;
        };
        output: string;
        meta: object;
    }>;
}>>, {
    keyPrefix: false;
}>">trpc</data-lsp>.<data-lsp lsp="(property) greeting: DecorateQueryProcedure<{
    input: {
        name: string;
    };
    output: string;
    transformer: false;
    errorShape: DefaultErrorShape;
    featureFlags: {
        keyPrefix: false;
    };
}> &amp; Record<string, never>">greeting</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) DecorateQueryProcedure<{ input: { name: string; }; output: string; transformer: false; errorShape: DefaultErrorShape; featureFlags: { keyPrefix: false; }; }>.queryOptions: TRPCQueryOptions
<string, string>(input: {
    name: string;
}, opts?: UnusedSkipTokenTRPCQueryOptionsIn<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> | undefined) => UnusedSkipTokenTRPCQueryOptionsOut<string, string, TRPCClientErrorLike<{
    transformer: false;
    errorShape: DefaultErrorShape;
}>, {
    keyPrefix: false;
}> (+2 overloads)">queryOptions</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) name: string">name</data-lsp>: </span><span style="color: #A5D6FF">'Jerry'</span><span style="color: #C9D1D9"> }));</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// greetingQuery.data === 'Hello Jerry'</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">null</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>With this new client we're removing a layer of abstraction which is a common source of confusion for new users, and instead providing a more direct way to work with TanStack React Query which will feel immediately familiar to those who are following TanStack's own <a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">documentation</a>. It also means we need less tRPC documentation to explain it, though we of course have <a href="https://trpc.io/docs/client/tanstack-react-query/setup">documentation to get you started</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="why-the-change">Why the change?<a href="https://trpc.io/blog/introducing-tanstack-react-query-client#why-the-change" class="hash-link" aria-label="Direct link to Why the change?" title="Direct link to Why the change?">​</a></h2>
<p>You can read our original RFC behind this change <a href="https://github.com/trpc/trpc/discussions/6240" target="_blank" rel="noopener noreferrer">here</a> which goes into some detail. But some of our key reasons are:</p>
<ul>
<li><strong>Simplicity</strong>: The new client is simpler and more TanStack Query-native, providing factories for common TanStack React Query interfaces like QueryKeys, QueryOptions, and MutationOptions. This lowers the learning curve as you can follow the <a href="https://tanstack.com/query/latest" target="_blank" rel="noopener noreferrer">official TanStack Query documentation</a></li>
<li><strong>Familiarity</strong>: The new client is more familiar to those who are already using TanStack Query, where you use TanStack Query for other workloads in your application doesn't force you to use an alternative syntax for tRPC</li>
<li><strong>React</strong>: Our classic React Query integration actually breaks the rules of hooks; it <a href="https://github.com/trpc/trpc/issues/2330" target="_blank" rel="noopener noreferrer">cannot be correctly linted</a>, and it encourages some patterns which will break under the React Compiler such as passing around hooks as props. In this respect the new client is more idiomatic React</li>
<li><strong>Maintainability</strong>: A challenge we've had with our versioning is keeping tRPC in lockstep with changes to TanStack Query, particularly new features which get added to QueryClient from time to time. By using the tiny surface area of native interfaces we can support React Query much more easily, while also following what <a href="https://bsky.app/profile/tkdodo.eu/post/3lgizrcvjmc24" target="_blank" rel="noopener noreferrer">TanStack's maintainers consider best practice</a></li>
<li><strong>Feedback</strong>: As we've said the classic client is a common cause of difficulty for new users, but also the feedback we had on <a href="https://github.com/trpc/trpc/discussions/6240" target="_blank" rel="noopener noreferrer">the RFC</a> for this client was overwhelmingly positive, with the majority of users who left us a comment or message excited to use this client. Of course not everyone is sold on the client right now, so we'll be keeping the classic client around</li>
</ul>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="whats-happening-to-the-classic-trpc-react-query-integration">What's happening to the classic tRPC React Query integration?<a href="https://trpc.io/blog/introducing-tanstack-react-query-client#whats-happening-to-the-classic-trpc-react-query-integration" class="hash-link" aria-label="Direct link to What's happening to the classic tRPC React Query integration?" title="Direct link to What's happening to the classic tRPC React Query integration?">​</a></h2>
<p>It's not going anywhere soon! We're committing to maintaining it for a long time to come, but it won't receive any significant new features and we'll consider it stable.</p>
<p>We still recommend new projects start with the new TanStack React Query integration, and existing projects to consider migrating over gradually.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="how-do-i-migrate">How do I migrate?<a href="https://trpc.io/blog/introducing-tanstack-react-query-client#how-do-i-migrate" class="hash-link" aria-label="Direct link to How do I migrate?" title="Direct link to How do I migrate?">​</a></h2>
<p>While the classic client is going to be maintained for a long time to come, we recommend new projects start with the new client and active projects consider gradually migrating over.</p>
<p>Both clients are compatible with each other and can exist in the same application, so you can migrate at your own pace. We are also working on a codemod which <strong>we would love</strong> community contributions to. We'd like to thank <a href="https://bsky.app/profile/reaper.is" target="_blank" rel="noopener noreferrer">@reaper</a> for his contributions to the codemod so far!</p>
<p>👉 <a href="https://trpc.io/docs/client/tanstack-react-query/migrating">Read the Migration Docs</a></p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Using Server Actions with tRPC]]></title>
            <link>https://trpc.io/blog/trpc-actions</link>
            <guid>https://trpc.io/blog/trpc-actions</guid>
            <pubDate>Thu, 23 May 2024 00:00:00 GMT</pubDate>
            <description><![CDATA[The builder-pattern for creating procedures which was introduced in tRPC v10 has been massively appreciated by the community, and many libraries have adopted similar patterns.]]></description>
            <content:encoded><![CDATA[<p>The builder-pattern for creating procedures which was introduced in tRPC v10 has been massively appreciated by the community, and many libraries have adopted similar patterns.
There's even been coined a term <code>tRPC like XYZ</code> as evidence of the growing popularity of this pattern. In fact, the other day I saw <a href="https://x.com/localhost_5173/status/1793259910723215835" target="_blank" rel="noopener noreferrer">someone
wondering if there was a way to write CLI applications with a similar API to tRPC</a>.
Sidenote, you can even use <a href="https://github.com/mmkal/trpc-cli" target="_blank" rel="noopener noreferrer">tRPC directly to do this</a>. But that's not what we're here to talk about today,
we're going to talk about how to use tRPC with server actions from Next.js.</p>
<!-- -->
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="whats-a-server-action">What's a server action?<a href="https://trpc.io/blog/trpc-actions#whats-a-server-action" class="hash-link" aria-label="Direct link to What's a server action?" title="Direct link to What's a server action?">​</a></h2>
<p>In case you live under a rock and haven't kept up with the latest <a href="https://react.dev/reference/rsc/server-actions" target="_blank" rel="noopener noreferrer">React</a> and <a href="https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations" target="_blank" rel="noopener noreferrer">Next.js</a> features, server actions allows you to write regular functions that are executed on the server, import them on the client and call them just as if they were regular functions.
You may think that this sounds similar to tRPC which is true. According to <a href="https://x.com/dan_abramov2" target="_blank" rel="noopener noreferrer">Dan Abramov</a>, server actions are tRPC as a bundler feature:</p>
<div class="mb-4 flex w-full justify-center"><div class="react-tweet-theme root_gzUe root_OzMz"><article class="article_zVA9"><span class="skeleton_XJfA" style="height:3rem;margin-bottom:0.75rem"></span><span class="skeleton_XJfA" style="height:6rem;margin:0.5rem 0"></span><div style="border-top:var(--tweet-border);margin:0.5rem 0"></div><span class="skeleton_XJfA" style="height:2rem"></span><span class="skeleton_XJfA" style="height:2rem;border-radius:9999px;margin-top:0.5rem"></span></article></div></div>
<p>And this is totally accurate, server actions are similar to tRPC, at the end of the day they're both <a href="https://en.wikipedia.org/wiki/Remote_procedure_call" target="_blank" rel="noopener noreferrer">RPCs</a>. Both allow you to write functions on the backend and call them with full typesafety on the frontend with the network layer abstracted away.</p>
<p>So where does tRPC come in? Why would I need both tRPC and server actions? Server actions is a primitive, and as for all primitives they're quite barebones and thus lacking some fundamental aspects when it comes to building APIs. For any API endpoint that is exposed over the network, you need to validate and authorize requests to ensure your API is not maliciously used. As previously mentioned, tRPC's API is appreciated by the community, so wouldn't it be nice if we could use tRPC to define server actions and utilize all the awesome features that come built-in with tRPC such as input validation, authentication and authorization through middlewares, output validation, data transformers, etc, etc? I think so, so let's dig in.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="defining-server-actions-with-trpc">Defining server actions with tRPC<a href="https://trpc.io/blog/trpc-actions#defining-server-actions-with-trpc" class="hash-link" aria-label="Direct link to Defining server actions with tRPC" title="Direct link to Defining server actions with tRPC">​</a></h2>
<div class="theme-admonition theme-admonition-note admonition_v5Ag alert alert--secondary"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path></svg></span>note</div><div class="admonitionContent_e2NW"><p><strong>Prerequisites:</strong> In order to use server actions, you need to use the Next.js App Router. Additionally, all the tRPC stuff we'll use are only available on tRPC v11, so make sure you're using the beta release channel of tRPC:</p><div class="tabs-container tabList_mG0D"><ul role="tablist" aria-orientation="horizontal" class="tabs"><li role="tab" tabindex="0" aria-selected="true" class="tabs__item tabItem_F3SZ tabs__item--active">npm</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">yarn</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">pnpm</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">bun</li><li role="tab" tabindex="-1" aria-selected="false" class="tabs__item tabItem_F3SZ">deno</li></ul><div class="margin-top--md"><div role="tabpanel" class="tabItem_f_Dr"><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">npm install @trpc/server<button type="button" aria-label="Copy code to clipboard" class="copy-button">Copy</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">yarn add @trpc/server<button type="button" aria-label="Copy code to clipboard" class="copy-button">Copy</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">pnpm add @trpc/server<button type="button" aria-label="Copy code to clipboard" class="copy-button">Copy</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">bun add @trpc/server<button type="button" aria-label="Copy code to clipboard" class="copy-button">Copy</button></pre></div><div role="tabpanel" class="tabItem_f_Dr" hidden=""><pre language="bash" title="shell" class="InstallationSnippet__CodeBlock">deno add npm:@trpc/server<button type="button" aria-label="Copy code to clipboard" class="copy-button">Copy</button></pre></div></div></div></div></div>
<p>Let's start off by initializing tRPC and defining our base server actions procedure.
We'll use the <code>experimental_caller</code> method on the procedure builder, which is a new method that allows you to
customize the way that the procedure is called when it's invoked as a function. We'll also use the adapter <code>experimental_nextAppDirCaller</code>
to make it compatible with Next.js. This adapter will handle cases where the server action is wrapped in <code>useActionState</code> on the client,
which <a href="https://react.dev/reference/react/useActionState#my-action-can-no-longer-read-the-submitted-form-data" target="_blank" rel="noopener noreferrer">changes the call signature of the server action</a>.</p>
<p>We'll also be using a <code>span</code> property as <a href="https://trpc.io/docs/server/metadata">metadata</a>, since there is no ordinary path like when you use a router (<code>user.byId</code> for example). You can use the span property to differentiate procedures, for example during logging or observability.</p>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #6F42C1">.</span><span style="color: #1976D2"><data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Next, we'll add some <a href="https://trpc.io/docs/server/context">context</a>. Since we wont be hosting a router using a regular HTTP adapter, we won't have any context injected through the <code>createContext</code>
method on the adapter. Instead, we'll use a middleware to inject our context. In this example, let's retrieve the current user from the session, and inject it into the context.</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./auth'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Inject user into context</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const user: User | null">user</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #24292EFF">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./auth'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const user: User | null">user</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp>: { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Lastly, we'll create a <code>protectedAction</code> procedure that will protect any action from unauthenticated users. If you have an existing middleware that does this you can use that, but I'll define one in-line for this example.</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/adapters/next-app-dir'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./auth'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">&gt;()</span><span style="color: #6F42C1">.<data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ <data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #24292EFF">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Inject user into context</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const user: User | null" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="meta-line"><span class="popover-prefix">           </span><span class="popover"><div class="arrow"></div>const user: User | null</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User | null; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User | null;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User | null;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) user: User | null">user</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'UNAUTHORIZED'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: <{
    user: User;
}>(opts: {
    ctx?: {
        user: User;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) ctx?: {
    user: User;
} | undefined">ctx</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">...</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #C2C3C5">// &lt;-- ensures type is non-nullable</span></div><div class="meta-line"><span class="popover-prefix">       </span><span class="popover"><div class="arrow"></div>(property) user: User</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>, <data-lsp lsp="(alias) class TRPCError
import TRPCError">TRPCError</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function experimental_nextAppDirCaller<TContext, TMeta>(config: Simplify<{
    pathExtractor?: (opts: {
        meta: TMeta;
    }) => string;
    normalizeFormData?: boolean;
    onError?: (opts: ErrorHandlerOptions<TContext>) => void;
} &amp; CreateContextCallback<TContext, () => MaybePromise<TContext>>>): CallerOverride<TContext>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/adapters/next-app-dir'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) function currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./auth'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) Meta.span: string">span</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const initTRPC: TRPCBuilder<object, object>
import initTRPC">initTRPC</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, object>.meta<Meta>(): TRPCBuilder<object, Meta>">meta</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">&gt;().</span><span style="color: #D2A8FF"><data-lsp lsp="(method) TRPCBuilder<object, Meta>.create<RuntimeConfigOptions<object, Meta>>(opts?: RuntimeConfigOptions<object, Meta> | undefined): TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">create</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const t: TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, {
    ctx: object;
    meta: Meta;
    errorShape: DefaultErrorShape;
    transformer: false;
}>">t</data-lsp>.<data-lsp lsp="(property) TRPCRootObject<object, Meta, RuntimeConfigOptions<object, Meta>, { ctx: object; meta: Meta; errorShape: DefaultErrorShape; transformer: false; }>.procedure: ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">procedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.experimental_caller(caller: CallerOverride<object>): ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">experimental_caller</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) experimental_nextAppDirCaller<object, unknown>(config: {
    pathExtractor?: ((opts: {
        meta: unknown;
    }) => string) | undefined;
    normalizeFormData?: boolean | undefined;
    onError?: ((opts: ErrorHandlerOptions<object>) => void) | undefined;
    createContext?: (() => MaybePromise<object>) | undefined;
}): CallerOverride<object>
import experimental_nextAppDirCaller">experimental_nextAppDirCaller</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(property) pathExtractor?: ((opts: {
    meta: unknown;
}) => string) | undefined">pathExtractor</data-lsp></span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) meta: unknown">meta</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> (<data-lsp lsp="(parameter) meta: unknown">meta</data-lsp> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Meta">Meta</data-lsp></span><span style="color: #C9D1D9">).<data-lsp lsp="(property) Meta.span: string">span</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User | null;
}>(fn: MiddlewareBuilder<{}, Meta, {
    user: User | null;
}, UnsetMarker> | MiddlewareFunction<object, Meta, object, {
    user: User | null;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const user: User | null" style="border-bottom: solid 2px lightgrey;">user</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) currentUser(): Promise<User | null>
import currentUser">currentUser</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="meta-line"><span class="popover-prefix">           </span><span class="popover"><div class="arrow"></div>const user: User | null</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {};
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<object>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User | null;
}>(opts: {
    ctx?: {
        user: User | null;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User | null;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) ctx?: {
    user: User | null;
} | undefined">ctx</data-lsp>: { <data-lsp lsp="(property) user: User | null">user</data-lsp> } });</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const serverActionProcedure: ProcedureBuilder<object, Meta, {
    user: User | null;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">serverActionProcedure</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User | null; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User | null;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User | null;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User | null">user</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">'UNAUTHORIZED'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: <{
    user: User;
}>(opts: {
    ctx?: {
        user: User;
    } | undefined;
    input?: unknown;
}) => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) ctx?: {
    user: User;
} | undefined">ctx</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp>: <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User | null;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User | null;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User | null;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User" style="border-bottom: solid 2px lightgrey;">user</data-lsp>, </span><span style="color: #8B949E">// &lt;-- ensures type is non-nullable</span></div><div class="meta-line"><span class="popover-prefix">       </span><span class="popover"><div class="arrow"></div>(property) user: User</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Alright, let's write an actual server action. Create an <code>_actions.ts</code> file, decorate it with the <code>"use server"</code> directive, and define your action.</p>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'../server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>">createPost</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    title: string;
}, UnsetMarker, void>): (input: {
    title: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    title: string;
}>">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// Do something with the input</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C2C3C5">// Since we're using the `experimental_caller`,</span></div><div class="line"><span style="color: #C2C3C5">// our procedure is now just an ordinary function:</span></div><div class="line"><span style="color: #24292EFF"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>" style="border-bottom: solid 2px lightgrey;">createPost</data-lsp>;</span></div><div class="meta-line"><span class="popover-prefix">    </span><span class="popover"><div class="arrow"></div>const createPost: (input: {
    title: string;
}) =&gt; Promise&lt;void&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'../server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>">createPost</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import protectedAction">protectedAction</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    title: string;
}, UnsetMarker, void>): (input: {
    title: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    title: string;
}>">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// Do something with the input</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #8B949E">// Since we're using the `experimental_caller`,</span></div><div class="line"><span style="color: #8B949E">// our procedure is now just an ordinary function:</span></div><div class="line"><span style="color: #C9D1D9"><data-lsp lsp="const createPost: (input: {
    title: string;
}) => Promise<void>" style="border-bottom: solid 2px lightgrey;">createPost</data-lsp>;</span></div><div class="meta-line"><span class="popover-prefix">    </span><span class="popover"><div class="arrow"></div>const createPost: (input: {
    title: string;
}) =&gt; Promise&lt;void&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Wow, it's that easy to define a server action that's protected from unauthenticated users, with input validation to protect against attacks such as SQL injections. Let's import this function on the client and call it.</p>
<div><pre class="shiki min-light with-title" style="background-color: #ffffff; color: #24292eff" title="app/post-form.tsx"><div class="code-title">app/post-form.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use client'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { createPost } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./_actions'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">PostForm</span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">    &lt;</span><span style="color: #22863A">form</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// Use `action` to make form progressively enhanced</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">action</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{createPost}</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// `Using `onSubmit` allows building rich interactive</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// forms once JavaScript has loaded</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">onSubmit</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (e) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #1976D2">e</span><span style="color: #6F42C1">.preventDefault</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">title</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">FormData</span><span style="color: #24292EFF">(</span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.currentTarget)</span><span style="color: #6F42C1">.get</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'title'</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Maybe show loading toast, etc etc. Endless possibilities</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createPost</span><span style="color: #24292EFF">({ title });</span></div><div class="line"><span style="color: #24292EFF">      }}</span></div><div class="line"><span style="color: #24292EFF">    &gt;</span></div><div class="line"><span style="color: #24292EFF">      &lt;</span><span style="color: #22863A">input</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">type</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"text"</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">name</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"title"</span><span style="color: #24292EFF"> /&gt;</span></div><div class="line"><span style="color: #24292EFF">      &lt;</span><span style="color: #22863A">button</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">type</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"submit"</span><span style="color: #24292EFF">&gt;Create Post&lt;/</span><span style="color: #22863A">button</span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">    &lt;/</span><span style="color: #22863A">form</span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">  );</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title" style="background-color: #0d1117; color: #c9d1d9" title="app/post-form.tsx"><div class="code-title">app/post-form.tsx</div><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use client'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { createPost } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./_actions'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">PostForm</span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">    &lt;</span><span style="color: #7EE787">form</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// Use `action` to make form progressively enhanced</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">action</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{createPost}</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// `Using `onSubmit` allows building rich interactive</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// forms once JavaScript has loaded</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">onSubmit</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657">e</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        e.</span><span style="color: #D2A8FF">preventDefault</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">title</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">FormData</span><span style="color: #C9D1D9">(e.currentTarget).</span><span style="color: #D2A8FF">get</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'title'</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Maybe show loading toast, etc etc. Endless possibilities</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createPost</span><span style="color: #C9D1D9">({ title });</span></div><div class="line"><span style="color: #C9D1D9">      }}</span></div><div class="line"><span style="color: #C9D1D9">    &gt;</span></div><div class="line"><span style="color: #C9D1D9">      &lt;</span><span style="color: #7EE787">input</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">type</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"text"</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">name</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"title"</span><span style="color: #C9D1D9"> /&gt;</span></div><div class="line"><span style="color: #C9D1D9">      &lt;</span><span style="color: #7EE787">button</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">type</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"submit"</span><span style="color: #C9D1D9">&gt;Create Post&lt;/</span><span style="color: #7EE787">button</span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">    &lt;/</span><span style="color: #7EE787">form</span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">  );</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="going-further">Going further<a href="https://trpc.io/blog/trpc-actions#going-further" class="hash-link" aria-label="Direct link to Going further" title="Direct link to Going further">​</a></h2>
<p>Using tRPC builders and it's composable way of defining reusable procedures, we can easily build more complex server actions. Below are some examples:</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="observability">Observability<a href="https://trpc.io/blog/trpc-actions#observability" class="hash-link" aria-label="Direct link to Observability" title="Direct link to Observability">​</a></h3>
<p>You can use <code>@baselime/node-opentelemtry</code>'s trpc plugin to add observability in just a few lines of code:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">diff</div><div class="code-container"><code><div class="line"><span style="color: #24292EFF">--- server/trpc.ts</span></div><div class="line"><span style="color: #24292EFF">+++ server/trpc.ts</span></div><div class="line"><span style="color: #24292EFF">+ import { tracing } from '@baselime/node-opentelemetry/trpc';</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  export const serverActionProcedure = t.procedure</span></div><div class="line"><span style="color: #24292EFF">    .experimental_caller(</span></div><div class="line"><span style="color: #24292EFF">      experimental_nextAppDirCaller({</span></div><div class="line"><span style="color: #24292EFF">        pathExtractor: (meta: Meta) =&gt; meta.span,</span></div><div class="line"><span style="color: #24292EFF">      }),</span></div><div class="line"><span style="color: #24292EFF">    )</span></div><div class="line"><span style="color: #24292EFF">    .use(async (opts) =&gt; {</span></div><div class="line"><span style="color: #24292EFF">      // Inject user into context</span></div><div class="line"><span style="color: #24292EFF">      const user = await currentUser();</span></div><div class="line"><span style="color: #24292EFF">      return opts.next({ ctx: { user } });</span></div><div class="line"><span style="color: #24292EFF">    })</span></div><div class="line"><span style="color: #24292EFF">+  .use(tracing());</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">--- app/_actions.ts</span></div><div class="line"><span style="color: #24292EFF">+++ app/_actions.ts</span></div><div class="line"><span style="color: #24292EFF">  export const createPost = protectedAction</span></div><div class="line"><span style="color: #24292EFF">+   .meta({ span: 'create-post' })</span></div><div class="line"><span style="color: #24292EFF">    .input(</span></div><div class="line"><span style="color: #24292EFF">      z.object({</span></div><div class="line"><span style="color: #24292EFF">        title: z.string(),</span></div><div class="line"><span style="color: #24292EFF">      }),</span></div><div class="line"><span style="color: #24292EFF">    )</span></div><div class="line"><span style="color: #24292EFF">    .mutation(async (opts) =&gt; {</span></div><div class="line"><span style="color: #24292EFF">      // Do something with the input</span></div><div class="line"><span style="color: #24292EFF">    });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">diff</div><div class="code-container"><code><div class="line"><span style="color: #FFA198">--- server/trpc.ts</span></div><div class="line"><span style="color: #7EE787">+++ server/trpc.ts</span></div><div class="line"><span style="color: #7EE787">+ import { tracing } from '@baselime/node-opentelemetry/trpc';</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  export const serverActionProcedure = t.procedure</span></div><div class="line"><span style="color: #C9D1D9">    .experimental_caller(</span></div><div class="line"><span style="color: #C9D1D9">      experimental_nextAppDirCaller({</span></div><div class="line"><span style="color: #C9D1D9">        pathExtractor: (meta: Meta) =&gt; meta.span,</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    )</span></div><div class="line"><span style="color: #C9D1D9">    .use(async (opts) =&gt; {</span></div><div class="line"><span style="color: #C9D1D9">      // Inject user into context</span></div><div class="line"><span style="color: #C9D1D9">      const user = await currentUser();</span></div><div class="line"><span style="color: #C9D1D9">      return opts.next({ ctx: { user } });</span></div><div class="line"><span style="color: #C9D1D9">    })</span></div><div class="line"><span style="color: #7EE787">+  .use(tracing());</span></div><div class="line"></div><div class="line"><span style="color: #FFA198">--- app/_actions.ts</span></div><div class="line"><span style="color: #7EE787">+++ app/_actions.ts</span></div><div class="line"><span style="color: #C9D1D9">  export const createPost = protectedAction</span></div><div class="line"><span style="color: #7EE787">+   .meta({ span: 'create-post' })</span></div><div class="line"><span style="color: #C9D1D9">    .input(</span></div><div class="line"><span style="color: #C9D1D9">      z.object({</span></div><div class="line"><span style="color: #C9D1D9">        title: z.string(),</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    )</span></div><div class="line"><span style="color: #C9D1D9">    .mutation(async (opts) =&gt; {</span></div><div class="line"><span style="color: #C9D1D9">      // Do something with the input</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Checkout <a href="https://github.com/baselime/node-opentelemetry/blob/main/TRPC.md" target="_blank" rel="noopener noreferrer">the Baselime tRPC Integration</a> for more information. Similar patterns should work for whatever observability playform you're using.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="rate-limiting">Rate Limiting<a href="https://trpc.io/blog/trpc-actions#rate-limiting" class="hash-link" aria-label="Direct link to Rate Limiting" title="Direct link to Rate Limiting">​</a></h3>
<p>You can use a service such as Unkey to rate limit your server actions. Here's an example of a protected server action that uses Unkey to rate limit the number of requests per user:</p>
<div></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) class Ratelimit
import Ratelimit">Ratelimit</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@unkey/ratelimit'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">rateLimitedAction</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new Ratelimit(config: RatelimitConfig): Ratelimit
import Ratelimit">Ratelimit</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) rootKey: string">rootKey</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var process: NodeJS.Process">process</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) NodeJS.Process.env: NodeJS.ProcessEnv">env</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="string | undefined">UNKEY_ROOT_KEY</data-lsp></span><span style="color: #D32F2F">!</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) async?: boolean | undefined">async</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">true</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) duration: number | Duration">duration</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'10s'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) limit: number">limit</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">5</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) namespace: string">namespace</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`trpc_</span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) path: string">path</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Ratelimit.limit(identifier: string, opts?: LimitOptions): Promise<RatelimitResponse>">limit</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) User.id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #1976D2"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) success: boolean">success</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'TOO_MANY_REQUESTS'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) message?: string | undefined">message</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>)</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(property) next: () => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="server/trpc.ts"><div class="code-title">server/trpc.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) class Ratelimit
import Ratelimit">Ratelimit</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@unkey/ratelimit'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">rateLimitedAction</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const protectedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">protectedAction</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.use<{
    user: User;
}>(fn: MiddlewareBuilder<{
    user: User;
}, Meta, {
    user: User;
}, UnsetMarker> | MiddlewareFunction<object, Meta, {
    user: User;
}, {
    user: User;
}, UnsetMarker>): ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>">use</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new Ratelimit(config: RatelimitConfig): Ratelimit
import Ratelimit">Ratelimit</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) rootKey: string">rootKey</data-lsp>: <data-lsp lsp="var process: NodeJS.Process">process</data-lsp>.<data-lsp lsp="(property) NodeJS.Process.env: NodeJS.ProcessEnv">env</data-lsp>.</span><span style="color: #79C0FF"><data-lsp lsp="string | undefined">UNKEY_ROOT_KEY</data-lsp></span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) async?: boolean | undefined">async</data-lsp>: </span><span style="color: #79C0FF">true</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) duration: number | Duration">duration</data-lsp>: </span><span style="color: #A5D6FF">'10s'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) limit: number">limit</data-lsp>: </span><span style="color: #79C0FF">5</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) namespace: string">namespace</data-lsp>: </span><span style="color: #A5D6FF">`trpc_${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) path: string">path</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> <data-lsp lsp="const unkey: Ratelimit">unkey</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Ratelimit.limit(identifier: string, opts?: LimitOptions): Promise<RatelimitResponse>">limit</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.<data-lsp lsp="(property) ctx: {
    user: User;
}">ctx</data-lsp>.<data-lsp lsp="(property) user: User">user</data-lsp>.<data-lsp lsp="(property) User.id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>.<data-lsp lsp="(property) success: boolean">success</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) code: &quot;UNAUTHORIZED&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;NOT_FOUND&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">'TOO_MANY_REQUESTS'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) message?: string | undefined">message</data-lsp>: </span><span style="color: #79C0FF"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #C9D1D9">.</span><span style="color: #79C0FF"><data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const ratelimit: RatelimitResponse">ratelimit</data-lsp>),</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">  }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: {
    ctx: {
        user: User;
    };
    type: ProcedureType;
    path: string;
    input: UnsetMarker;
    getRawInput: GetRawInputFn;
    meta: Meta | undefined;
    signal: AbortSignal | undefined;
    batchIndex: number;
    next: {
        (): Promise<MiddlewareResult<{
            user: User;
        }>>;
        <$ContextOverride>(opts: {
            ctx?: $ContextOverride | undefined;
            input?: unknown;
        }): Promise<MiddlewareResult<$ContextOverride>>;
        (opts: {
            getRawInput: GetRawInputFn;
        }): Promise<...>;
    };
}">opts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(property) next: () => Promise<MiddlewareResult<{
    user: User;
}>> (+2 overloads)">next</data-lsp></span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<div><pre class="shiki min-light with-title twoslash lsp" style="background-color: #ffffff; color: #24292eff" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #22863A">'use server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'zod'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'../server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const commentOnPost: (input: {
    postId: string;
    content: string;
}) => Promise<void>">commentOnPost</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, {
    postId: string;
    content: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    postId: z.ZodString;
    content: z.ZodString;
}>(shape?: {
    postId: z.ZodString;
    content: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) postId: z.ZodString">postId</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) content: z.ZodString">content</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { postId: string; content: string; }, { postId: string; content: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, UnsetMarker, void>): (input: {
    postId: string;
    content: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #1976D2"><data-lsp lsp="namespace console
var console: Console">console</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #22863A">`</span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) User.name: string">name</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A"> commented on </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) postId: string">postId</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A"> saying </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) content: string">content</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    );</span></div><div class="line"><span style="color: #24292EFF">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark with-title twoslash lsp" style="background-color: #0d1117; color: #c9d1d9" title="app/_actions.ts"><div class="code-title">app/_actions.ts</div><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #A5D6FF">'use server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="import z">z</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'zod'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'../server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const commentOnPost: (input: {
    postId: string;
    content: string;
}) => Promise<void>">commentOnPost</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(alias) const rateLimitedAction: ProcedureBuilder<object, Meta, {
    user: User;
}, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>
import rateLimitedAction">rateLimitedAction</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, true>.input<z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, {
    postId: string;
    content: string;
}, UnsetMarker, UnsetMarker, true>">input</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    postId: z.ZodString;
    content: z.ZodString;
}>(shape?: {
    postId: z.ZodString;
    content: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    postId: z.ZodString;
    content: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) postId: z.ZodString">postId</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) content: z.ZodString">content</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  )</span></div><div class="line"><span style="color: #C9D1D9">  .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, Meta, { user: User; }, { postId: string; content: string; }, { postId: string; content: string; }, UnsetMarker, UnsetMarker, true>.mutation<void>(resolver: ProcedureResolver<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}, UnsetMarker, void>): (input: {
    postId: string;
    content: string;
}) => Promise<void>">mutation</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="namespace console
var console: Console">console</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Console.log(message?: any, ...optionalParams: any[]): void (+1 overload)">log</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #A5D6FF">`${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.ctx: {
    user: User;
}">ctx</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) user: User">user</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) User.name: string">name</data-lsp></span><span style="color: #A5D6FF">} commented on ${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) postId: string">postId</data-lsp></span><span style="color: #A5D6FF">} saying ${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProcedureResolverOptions<object, Meta, {
    user: User;
}, {
    postId: string;
    content: string;
}>">opts</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) ProcedureResolverOptions<object, Meta, { user: User; }, { postId: string; content: string; }>.input: {
    postId: string;
    content: string;
}">input</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) content: string">content</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    );</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Read more on rate limiting your tRPC procedures <a href="https://www.unkey.com/blog/ratelimit-trpc-routes" target="_blank" rel="noopener noreferrer">in this post by the folks over at Unkey</a>.</p>
<p>The possibilities are endless, and I bet you already got a ton of nice utility middlewares you're using in your tRPC applications today. If not, you might found some out there you can <code>npm install</code>!</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="wrapping-up">Wrapping up<a href="https://trpc.io/blog/trpc-actions#wrapping-up" class="hash-link" aria-label="Direct link to Wrapping up" title="Direct link to Wrapping up">​</a></h2>
<p>Server Actions are by no means a silver bullet. In places that requires more dynamic data, you might want to keep your data in the client-side React Query cache, and do mutations using <code>useMutation</code> instead. That's totally valid. These new primitives should also be easy to incrementally adopt, so you can move individual procedures over from your existing tRPC API to server actions in places where it makes sense to do so. There's no need to rewrite your entire API.</p>
<p>By defining your server actions using tRPC, you can share a lot of the same logic you're using today and choose where you expose the mutation as a server action or as a more traditional mutation. You as a developer have the power to pick what patterns works best for your application. In case you're not using tRPC today, there are some packages (<a href="https://github.com/TheEdoRan/next-safe-action" target="_blank" rel="noopener noreferrer">next-safe-action</a> and <a href="https://github.com/IdoPesok/zsa" target="_blank" rel="noopener noreferrer">zsa</a> comes to mind) that let's you define type-safe, input validated server actions worth checking out as well.</p>
<p>If you wanna see an app using this in action, check out <a href="https://trellix-trpc.vercel.app/" target="_blank" rel="noopener noreferrer">Trellix tRPC</a>, an app I made recently utilizing these new primitives.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="what-do-you-think-we-want-your-feedback">What do you think? We want your feedback<a href="https://trpc.io/blog/trpc-actions#what-do-you-think-we-want-your-feedback" class="hash-link" aria-label="Direct link to What do you think? We want your feedback" title="Direct link to What do you think? We want your feedback">​</a></h3>
<p>So, what do you think? Let us know over at <a href="https://github.com/trpc/trpc/discussions/5737" target="_blank" rel="noopener noreferrer">Github</a> and help us iterate to get these primitives to a stable state.</p>
<p>There's still some work to be done, especially regarding error handling. Next.js advocates for returning errors, and we'd like to make this as typesafe as possible. Check out <a href="https://github.com/trpc/trpc/pull/5554" target="_blank" rel="noopener noreferrer">this WIP PR by Alex</a> for some early work on this.</p>
<p>Until next time, happy coding!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Writing a tiny tRPC client]]></title>
            <link>https://trpc.io/blog/tinyrpc-client</link>
            <guid>https://trpc.io/blog/tinyrpc-client</guid>
            <pubDate>Tue, 17 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[Ever wondered how tRPC works? Maybe you want to start contributing to the project but you're frightened by the internals? The aim of this post is to familiarize you with the internals of tRPC by writing a minimal client that covers the big parts of how tRPC works.]]></description>
            <content:encoded><![CDATA[<p>Ever wondered how tRPC works? Maybe you want to start contributing to the project but you're frightened by the internals? The aim of this post is to familiarize you with the internals of tRPC by writing a minimal client that covers the big parts of how tRPC works.</p>
<!-- -->
<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_e2NW"><p>It's recommended that you understand some of the core concepts in TypeScript such as generics, conditional types, the <code>extends</code> keyword and recursion. If you're not familiar with these, I recommend going through <a href="https://twitter.com/mattpocockuk" target="_blank" rel="noopener noreferrer">Matt Pocock</a>'s <a href="https://github.com/total-typescript/beginners-typescript-tutorial" target="_blank" rel="noopener noreferrer">Beginner TypeScript</a> tutorial to get familiar with these concepts before reading on.</p></div></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="overview">Overview<a href="https://trpc.io/blog/tinyrpc-client#overview" class="hash-link" aria-label="Direct link to Overview" title="Direct link to Overview">​</a></h2>
<p>Let's assume we have a simple tRPC router with three procedures that looks like this:</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">; <data-lsp lsp="(property) title: string">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF"> };</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #24292EFF">[] </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [];</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>(_: {
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) post: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>>">post</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const router: RouterBuilder
<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>(_: {
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) byId: QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">byId</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    id: string;
}, {
    id: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) id: z.ZodString">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { id: string; }, { id: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    id: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) p: Post">p</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) id: string">id</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"NOT_FOUND"</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) byTitle: QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">byTitle</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) p: Post">p</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) title: string">title</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) title: string">title</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"NOT_FOUND"</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    <data-lsp lsp="(property) create: MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">create</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #24292EFF">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="import z">z</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #24292EFF">() }))</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">.<data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.mutation<{
    title: string;
    id: string;
}>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, {
    title: string;
    id: string;
}>): MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">mutation</data-lsp></span><span style="color: #24292EFF">(({ <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> { <data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const uuid: () => string">uuid</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">...</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> };</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #1976D2"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<Post>.push(...items: Post[]): number">push</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> { </span><span style="color: #FFA657"><data-lsp lsp="(property) id: string">id</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">; </span><span style="color: #FFA657"><data-lsp lsp="(property) title: string">title</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9"> };</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const posts: Post[]">posts</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Post = {
    id: string;
    title: string;
}">Post</data-lsp></span><span style="color: #C9D1D9">[] </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> [];</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const appRouter: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>>">appRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}>(_: {
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
        byTitle: QueryProcedure<{
            input: {
                title: string;
            };
            output: Post;
            meta: object;
        }>;
        create: MutationProcedure<{
            input: {
                title: string;
            };
            output: {
                title: string;
                id: string;
            };
            meta: object;
        }>;
    }>>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(property) post: BuiltRouter<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>>">post</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const router: RouterBuilder
<{
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}>(_: {
    byId: QueryProcedure<{
        input: {
            id: string;
        };
        output: Post;
        meta: object;
    }>;
    byTitle: QueryProcedure<{
        input: {
            title: string;
        };
        output: Post;
        meta: object;
    }>;
    create: MutationProcedure<{
        input: {
            title: string;
        };
        output: {
            title: string;
            id: string;
        };
        meta: object;
    }>;
}) => BuiltRouter<...>">router</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) byId: QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">byId</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    id: string;
}, {
    id: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    id: z.ZodString;
}>(shape?: {
    id: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    id: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) id: z.ZodString">id</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { id: string; }, { id: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    id: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        id: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) p: Post">p</data-lsp>.<data-lsp lsp="(property) id: string">id</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) input: {
    id: string;
}">input</data-lsp>.<data-lsp lsp="(property) id: string">id</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">"NOT_FOUND"</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) byTitle: QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">byTitle</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.query<Post>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, Post>): QueryProcedure<{
    input: {
        title: string;
    };
    output: Post;
    meta: object;
}>">query</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: Post | undefined">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.find(predicate: (value: Post, index: number, obj: Post[]) => unknown, thisArg?: any): Post | undefined (+1 overload)">find</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) p: Post">p</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) p: Post">p</data-lsp>.<data-lsp lsp="(property) title: string">title</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp>.<data-lsp lsp="(property) title: string">title</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"><data-lsp lsp="const post: Post | undefined">post</data-lsp>) </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(alias) new TRPCError(opts: {
    message?: string;
    code: TRPC_ERROR_CODE_KEY;
    cause?: unknown;
}): TRPCError
import TRPCError">TRPCError</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) code: &quot;NOT_FOUND&quot; | &quot;PARSE_ERROR&quot; | &quot;BAD_REQUEST&quot; | &quot;INTERNAL_SERVER_ERROR&quot; | &quot;NOT_IMPLEMENTED&quot; | &quot;BAD_GATEWAY&quot; | &quot;SERVICE_UNAVAILABLE&quot; | &quot;GATEWAY_TIMEOUT&quot; | &quot;UNAUTHORIZED&quot; | &quot;PAYMENT_REQUIRED&quot; | &quot;FORBIDDEN&quot; | &quot;METHOD_NOT_SUPPORTED&quot; | &quot;TIMEOUT&quot; | &quot;CONFLICT&quot; | &quot;PRECONDITION_FAILED&quot; | &quot;PAYLOAD_TOO_LARGE&quot; | &quot;UNSUPPORTED_MEDIA_TYPE&quot; | &quot;UNPROCESSABLE_CONTENT&quot; | &quot;PRECONDITION_REQUIRED&quot; | &quot;TOO_MANY_REQUESTS&quot; | &quot;CLIENT_CLOSED_REQUEST&quot;">code</data-lsp>: </span><span style="color: #A5D6FF">"NOT_FOUND"</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: Post">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">    <data-lsp lsp="(property) create: MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">create</data-lsp>: <data-lsp lsp="const publicProcedure: ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>">publicProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, UnsetMarker, UnsetMarker, UnsetMarker, UnsetMarker, false>.input<z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>>(schema: z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>): ProcedureBuilder<object, object, object, {
    title: string;
}, {
    title: string;
}, UnsetMarker, UnsetMarker, false>">input</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function object<{
    title: z.ZodString;
}>(shape?: {
    title: z.ZodString;
} | undefined, params?: string | {
    error?: string | z.core.$ZodErrorMap<NonNullable<z.core.$ZodIssueUnrecognizedKeys | z.core.$ZodIssueInvalidType<unknown>>> | undefined;
    message?: string | undefined | undefined;
} | undefined): z.ZodObject<{
    title: z.ZodString;
}, z.core.$strip>">object</data-lsp></span><span style="color: #C9D1D9">({ <data-lsp lsp="(property) title: z.ZodString">title</data-lsp>: <data-lsp lsp="import z">z</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="function string(params?: string | z.core.$ZodStringParams): z.ZodString (+1 overload)">string</data-lsp></span><span style="color: #C9D1D9">() }))</span></div><div class="line"><span style="color: #C9D1D9">      .</span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProcedureBuilder<object, object, object, { title: string; }, { title: string; }, UnsetMarker, UnsetMarker, false>.mutation<{
    title: string;
    id: string;
}>(resolver: ProcedureResolver<object, object, object, {
    title: string;
}, UnsetMarker, {
    title: string;
    id: string;
}>): MutationProcedure<{
    input: {
        title: string;
    };
    output: {
        title: string;
        id: string;
    };
    meta: object;
}>">mutation</data-lsp></span><span style="color: #C9D1D9">(({ </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp></span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> { <data-lsp lsp="(property) id: string">id</data-lsp>: </span><span style="color: #D2A8FF"><data-lsp lsp="const uuid: () => string">uuid</data-lsp></span><span style="color: #C9D1D9">(), </span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) input: {
    title: string;
}">input</data-lsp> };</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="const posts: Post[]">posts</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<Post>.push(...items: Post[]): number">push</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const post: {
    title: string;
    id: string;
}">post</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>The goal of our client is to mimic this object structure on our client so that we can call procedures like:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post1</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byId</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'123'</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post2</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byTitle</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ title</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Hello world'</span><span style="color: #24292EFF"> });</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">newPost</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">create</span><span style="color: #6F42C1">.mutate</span><span style="color: #24292EFF">({ title</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Foo'</span><span style="color: #24292EFF"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post1</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byId.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ id: </span><span style="color: #A5D6FF">'123'</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post2</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byTitle.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ title: </span><span style="color: #A5D6FF">'Hello world'</span><span style="color: #C9D1D9"> });</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">newPost</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.create.</span><span style="color: #D2A8FF">mutate</span><span style="color: #C9D1D9">({ title: </span><span style="color: #A5D6FF">'Foo'</span><span style="color: #C9D1D9"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>To do this, tRPC uses a combination of <a href="https://mdn.io/proxy" target="_blank" rel="noopener noreferrer"><code>Proxy</code></a>-objects and some TypeScript magic to augment the object structure with the <code>.query</code> and <code>.mutate</code> methods on them - meaning we actually LIE to you about what you're doing (more on that later) in order to provide an excellent developer experience!</p>
<p>On a high level, what we want to do is to map <code>post.byId.query()</code> to a GET request to our server, and <code>post.create.mutate()</code> to a POST request, and the types should all be propagated from back to front. So, how do we do this?</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="implementing-a-tiny-trpc-client">Implementing a tiny tRPC client<a href="https://trpc.io/blog/tinyrpc-client#implementing-a-tiny-trpc-client" class="hash-link" aria-label="Direct link to Implementing a tiny tRPC client" title="Direct link to Implementing a tiny tRPC client">​</a></h2>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="️-the-typescript-magic">🧙‍♀️ The TypeScript magic<a href="https://trpc.io/blog/tinyrpc-client#%EF%B8%8F-the-typescript-magic" class="hash-link" aria-label="Direct link to 🧙‍♀️ The TypeScript magic" title="Direct link to 🧙‍♀️ The TypeScript magic">​</a></h3>
<p>Let's start with the fun TypeScript magic to unlock the awesome autocompletion and typesafety we all know and love from using tRPC.</p>
<p>We'll need to use recursive types so that we can infer arbitrary deep router structures. Also, we know that we want our procedures <code>post.byId</code> and <code>post.create</code> to have the <code>.query</code> and <code>.mutate</code> methods on them respectively - in tRPC, we call this decorating the procedures. In <code>@trpc/server</code>, we have some inference helpers that will infer the input and output types of our procedures with these resolved methods, which we'll use to infer the types for these functions, so let's write some code!</p>
<p>Let's consider what we want to achieve to provide autocompletion on paths as well as inference of the procedures input and output types:</p>
<ul>
<li>If we're on a router, we want to be able to access it's sub-routers and procedures. (we'll get to this in a little bit)</li>
<li>If we're on a query procedure, we want to be able to call <code>.query</code> on it.</li>
<li>If we're on a mutation procedure, we want to be able to call <code>.mutate</code> on it.</li>
<li>If we're trying to access anything else, we want to get a type error indicating that procedure doesn't exist on the backend.</li>
</ul>
<p>So let's create a type that will do this for us:</p>
<div></div>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) query: Resolver<TProcedure>">query</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) mutate: Resolver<TProcedure>">mutate</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FFA657"><data-lsp lsp="(property) query: Resolver<TProcedure>">query</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FFA657"><data-lsp lsp="(property) mutate: Resolver<TProcedure>">mutate</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type DecorateProcedure<TProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>We'll use some of tRPC's built-in inference helpers to infer the input and output types of our procedures to define the <code>Resolver</code> type.</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(parameter) input: inferProcedureInput<TProcedure>">input</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface Promise<T>">Promise</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #24292EFF">&gt;&gt;;</span></div><div class="line">&nbsp;</div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCQueryProcedure = QueryProcedure<any>
import AnyTRPCQueryProcedure">AnyTRPCQueryProcedure</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">  <data-lsp lsp="(alias) type AnyTRPCMutationProcedure = MutationProcedure<any>
import AnyTRPCMutationProcedure">AnyTRPCMutationProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">} </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(parameter) input: inferProcedureInput<TProcedure>">input</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type inferProcedureInput<TProcedure extends AnyProcedure> = undefined extends inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] ? void | inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;] : inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;input&quot;]
import inferProcedureInput">inferProcedureInput</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface Promise<T>">Promise</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(alias) type inferProcedureOutput<TProcedure> = inferProcedureParams<TProcedure>[&quot;$types&quot;][&quot;output&quot;]
import inferProcedureOutput">inferProcedureOutput</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TProcedure in type Resolver<TProcedure extends AnyTRPCProcedure>">TProcedure</data-lsp></span><span style="color: #C9D1D9">&gt;&gt;;</span></div><div class="line">&nbsp;</div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Let's try this out on our <code>post.byId</code> procedure:</p>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type PostById = (input: {
    id: string;
}) => Promise<Post>" style="border-bottom: solid 2px lightgrey;">PostById</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>> &amp; DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>">AppRouter</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #22863A">'post'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'byId'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="meta-line"><span class="popover-prefix">        </span><span class="popover"><div class="arrow"></div>type PostById = (input: {
    id: string;
}) =&gt; Promise&lt;Post&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type PostById = (input: {
    id: string;
}) => Promise<Post>" style="border-bottom: solid 2px lightgrey;">PostById</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type Resolver<TProcedure extends AnyTRPCProcedure> = (input: inferProcedureInput<TProcedure>) => Promise<inferProcedureOutput<TProcedure>>">Resolver</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="type AppRouter = Router<{
    ctx: object;
    meta: object;
    errorShape: DefaultErrorShape;
    transformer: false;
}, DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>> &amp; DecorateCreateRouterOptions<{
    post: BuiltRouter<{
        ctx: object;
        meta: object;
        errorShape: DefaultErrorShape;
        transformer: false;
    }, DecorateCreateRouterOptions<{
        byId: QueryProcedure<{
            input: {
                id: string;
            };
            output: Post;
            meta: object;
        }>;
    }>>;
}>">AppRouter</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'post'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'byId'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="meta-line"><span class="popover-prefix">        </span><span class="popover"><div class="arrow"></div>type PostById = (input: {
    id: string;
}) =&gt; Promise&lt;Post&gt;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Nice, that's what we expected - we can now call <code>.query</code> on our procedure and get the correct input and output types inferred!</p>
<p>Finally, we'll create a type that will recursively traverse the router and decorate all procedures along the way:</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"@trpc/server"</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">"@trpc/server"</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  [</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF">]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">infer</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #24292EFF">&gt;</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">};</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">"@trpc/server"</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">"@trpc/server"</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  [</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9">]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRecord in type DecorateRouterRecord<TRecord extends TRPCRouterRecord>">TRecord</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TKey">TKey</data-lsp></span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">infer</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) interface TRPCRouterRecord
import TRPCRouterRecord">TRPCRouterRecord</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCProcedure = AnyTRPCQueryProcedure | AnyTRPCMutationProcedure | AnySubscriptionProcedure
import AnyTRPCProcedure">AnyTRPCProcedure</data-lsp></span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateProcedure<TProcedure> = TProcedure extends AnyTRPCQueryProcedure ? {
    query: Resolver<TProcedure>;
} : TProcedure extends AnyTRPCMutationProcedure ? {
    mutate: Resolver<TProcedure>;
} : never">DecorateProcedure</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) $Value">$Value</data-lsp></span><span style="color: #C9D1D9">&gt;</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">};</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Let's digest this type a bit:</p>
<ol>
<li>We pass a <code>TRPCRouterRecord</code> to the type as a generic, which is a type containing all the procedures and sub-routers that exists on a tRPC router.</li>
<li>We iterate over the keys of the record, which are the procedure or router names, and do the following:<!-- -->
<ul>
<li>If the key maps to a router, we recursively call the type on that router's procedure record, which will decorate all the procedures in that router. This will provide autocompletion as we traverse the path.</li>
<li>If the key maps to a procedure, we decorate the procedure using the <code>DecorateProcedure</code> type we created earlier.</li>
<li>If the key doesn't map to a procedure or router, we assign the <code>never</code> type which is like saying "this key doesn't exist" which will cause a type error if we try to access it.</li>
</ul>
</li>
</ol>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="-the-proxy-remapping">🤯 The Proxy remapping<a href="https://trpc.io/blog/tinyrpc-client#-the-proxy-remapping" class="hash-link" aria-label="Direct link to 🤯 The Proxy remapping" title="Direct link to 🤯 The Proxy remapping">​</a></h3>
<p>Now that we got all the types setup, we need to actually implement the functionality which will augment the server's router definition on the client so we can invoke the procedures like normal functions.</p>
<p>We'll first create a helper function for creating recursive proxies - <code>createRecursiveProxy</code>:</p>
<div class="theme-admonition theme-admonition-info admonition_v5Ag alert alert--info"><div class="admonitionHeading_usrK"><span class="admonitionIcon_bgEp"><svg viewBox="0 0 14 16"><path fill-rule="evenodd" d="M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"></path></svg></span>info</div><div class="admonitionContent_e2NW"><p>This is almost the exact implementation used in production, with the exception that we aren't handling some edge cases. <a href="https://github.com/trpc/trpc/blob/main/packages/server/src/shared/createProxy/index.ts" target="_blank" rel="noopener noreferrer">See for yourself</a>!</p></div></div>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">interface</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">[];</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF">[];</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">readonly</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF">[]) {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const proxy: unknown">proxy</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">unknown</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Proxy: ProxyConstructor
new <() => void>(target: () => void, handler: ProxyHandler<() => void>) => () => void">Proxy</data-lsp></span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    () </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// dummy no-op function since we don't have any</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #C2C3C5">// client-side target we want to remap to</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(method) ProxyHandler<() => void>.get?(target: () => void, p: string | symbol, receiver: any): any">get</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) _obj: () => void">_obj</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'string'</span><span style="color: #24292EFF">) </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Recursively compose the full path until a function is invoked</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">...</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) key: string">key</data-lsp>]);</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1"><data-lsp lsp="(method) ProxyHandler<() => void>.apply?(target: () => void, thisArg: any, argArray: any[]): any">apply</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="(parameter) _1: () => void">_1</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) _2: any">_2</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> <data-lsp lsp="(parameter) args: any[]">args</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// Call the callback function with the entire path we</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #C2C3C5">// recursively created and forward the arguments</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(parameter) callback: (opts: ProxyCallbackOptions) => unknown">callback</data-lsp></span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">          <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">          <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">        });</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  );</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> <data-lsp lsp="const proxy: unknown">proxy</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">interface</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">[];</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9">[];</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="interface ProxyCallbackOptions">ProxyCallbackOptions</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type ProxyCallback = (opts: ProxyCallbackOptions) => unknown">ProxyCallback</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">readonly</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">[]) {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const proxy: unknown">proxy</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">unknown</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Proxy: ProxyConstructor
new <() => void>(target: () => void, handler: ProxyHandler<() => void>) => () => void">Proxy</data-lsp></span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    () </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// dummy no-op function since we don't have any</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #8B949E">// client-side target we want to remap to</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">    {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProxyHandler<() => void>.get?(target: () => void, p: string | symbol, receiver: any): any">get</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) _obj: () => void">_obj</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) key: string | symbol">key</data-lsp></span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) key: string | symbol">key</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'string'</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Recursively compose the full path until a function is invoked</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="(parameter) callback: ProxyCallback">callback</data-lsp>, [</span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) path: readonly string[]">path</data-lsp>, <data-lsp lsp="(parameter) key: string">key</data-lsp>]);</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #D2A8FF"><data-lsp lsp="(method) ProxyHandler<() => void>.apply?(target: () => void, thisArg: any, argArray: any[]): any">apply</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FFA657"><data-lsp lsp="(parameter) _1: () => void">_1</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) _2: any">_2</data-lsp></span><span style="color: #C9D1D9">, </span><span style="color: #FFA657"><data-lsp lsp="(parameter) args: any[]">args</data-lsp></span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// Call the callback function with the entire path we</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #8B949E">// recursively created and forward the arguments</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="(parameter) callback: (opts: ProxyCallbackOptions) => unknown">callback</data-lsp></span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">          <data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">          <data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">        });</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">    },</span></div><div class="line"><span style="color: #C9D1D9">  );</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const proxy: unknown">proxy</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">}</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>This looks a bit magical, what does this do?</p>
<ul>
<li>The <code>get</code> method handles property accesses such as <code>post.byId</code>. The key is the property name we're accessing, so when we type <code>post</code> our <code>key</code> will be <code>post</code>, and when we type <code>post.byId</code> our <code>key</code> will be <code>byId</code>. The recursive proxy combines all of these keys into a final path, e.g. ["post", "byId", "query"], that we can use to determine the URL we want to send a request to.</li>
<li>The <code>apply</code> method is called when we invoke a function on the proxy, such as <code>.query(args)</code>. The <code>args</code> is the arguments we pass to the function, so when we call <code>post.byId.query(args)</code> our <code>args</code> will be our input, which we'll provide as query parameters or request body depending on the type of procedure. The <code>createRecursiveProxy</code> takes in a callback function which we'll map the <code>apply</code> to with the path and args.</li>
</ul>
<p>Below is a visual representation of how the proxy works on the call <code>trpc.post.byId.query({ id: 1 })</code>:</p>
<p><img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-17-tinyrpc-client/proxy.png" alt="proxy" class="img_Njog"></p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="-putting-it-all-together">🧩 Putting it all together<a href="https://trpc.io/blog/tinyrpc-client#-putting-it-all-together" class="hash-link" aria-label="Direct link to 🧩 Putting it all together" title="Direct link to 🧩 Putting it all together">​</a></h3>
<p>Now that we have this helper and know what it does, let's use it to create our client. We'll provide the <code>createRecursiveProxy</code> a callback that will take the path and args and request the server using <code>fetch</code>. We'll need to add a generic to the function that will accept any tRPC router type (<code>AnyTRPCRouter</code>), and then we'll cast the return type to the <code>DecorateRouterRecord</code> type we created earlier:</p>
<div></div>
<div><pre class="shiki min-light twoslash lsp" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> { <data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp> } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@trpc/server/rpc'</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="const createTinyRPCClient: <TRouter extends AnyTRPCRouter>(baseUrl: string) => DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">createTinyRPCClient</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> &lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp></span><span style="color: #24292EFF">&gt;(</span></div><div class="line"><span style="color: #24292EFF">  <data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">) </span><span style="color: #D32F2F">=&gt;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> (<data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> [</span><span style="color: #D32F2F">...</span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>]; </span><span style="color: #C2C3C5">// e.g. ["post", "byId", "query"]</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<string>.pop(): string | undefined">pop</data-lsp></span><span style="color: #24292EFF">()</span><span style="color: #D32F2F">!</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'mutate'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Array<string>.join(separator?: string): string">join</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">'.'</span><span style="color: #24292EFF">); </span><span style="color: #C2C3C5">// "post.byId" - this is the path procedures have on the backend</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`</span><span style="color: #D32F2F">${</span><span style="color: #24292EFF"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">/</span><span style="color: #D32F2F">${</span><span style="color: #24292EFF"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> [</span><span style="color: #1976D2"><data-lsp lsp="const input: unknown">input</data-lsp></span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="const input: unknown">input</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">&amp;&amp;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const input: {} | null">input</data-lsp>);</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">let</span><span style="color: #24292EFF"> <data-lsp lsp="let body: string | undefined">body</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">undefined</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">|</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">string</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (<data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp> </span><span style="color: #D32F2F">!==</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">false</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (<data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF">) {</span></div><div class="line"><span style="color: #24292EFF">        <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #D32F2F">+=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`?input=</span><span style="color: #D32F2F">${</span><span style="color: #6F42C1"><data-lsp lsp="function encodeURIComponent(uriComponent: string | number | boolean): string">encodeURIComponent</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>)</span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">      } </span><span style="color: #D32F2F">else</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        <data-lsp lsp="let body: string | undefined">body</data-lsp> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> <data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">      }</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCResponse">json</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp></span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>">fetch</data-lsp></span><span style="color: #24292EFF">(<data-lsp lsp="let uri: string">uri</data-lsp></span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.method?: string | undefined">method</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> <data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'query'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'GET'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'POST'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.headers?: HeadersInit | undefined">headers</data-lsp></span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #22863A">'Content-Type'</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'application/json'</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      <data-lsp lsp="(property) RequestInit.body?: BodyInit | null | undefined">body</data-lsp></span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #6F42C1">.<data-lsp lsp="(method) Promise<Response>.then<any, TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown> | PromiseLike<TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>) | null | undefined): Promise<...>">then</data-lsp></span><span style="color: #24292EFF">((<data-lsp lsp="(parameter) res: Response">res</data-lsp>) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="(parameter) res: Response">res</data-lsp></span><span style="color: #6F42C1">.<data-lsp lsp="(method) Body.json(): Promise<any>">json</data-lsp></span><span style="color: #24292EFF">());</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #22863A">'error'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> <data-lsp lsp="const json: TRPCResponse">json</data-lsp>) {</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">throw</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">new</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="var Error: ErrorConstructor
new (message?: string) => Error">Error</data-lsp></span><span style="color: #24292EFF">(</span><span style="color: #22863A">`Error: </span><span style="color: #D32F2F">${</span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCErrorResponse<TRPCErrorShape<object>>">json</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) JSONRPC2.ErrorResponse<TRPCErrorShape<object>>.error: TRPCErrorShape<object>">error</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCErrorShape<object>.message: string">message</data-lsp></span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #24292EFF">);</span></div><div class="line"><span style="color: #24292EFF">    }</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// No error - all good. Return the data.</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> </span><span style="color: #1976D2"><data-lsp lsp="const json: TRPCSuccessResponse<unknown>">json</data-lsp></span><span style="color: #24292EFF">.</span><span style="color: #1976D2"><data-lsp lsp="(property) JSONRPC2.ResultResponse<TRPCResult<unknown>>.result: TRPCResult<unknown>">result</data-lsp></span><span style="color: #24292EFF">.<data-lsp lsp="(property) TRPCResult<unknown>.data: unknown">data</data-lsp>;</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span><span style="color: #24292EFF"> []) </span><span style="color: #D32F2F">as</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #C2C3C5">//   ^? provide empty array as path to begin with</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark twoslash lsp" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> { <data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp> } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'@trpc/server/rpc'</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="const createTinyRPCClient: <TRouter extends AnyTRPCRouter>(baseUrl: string) => DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">createTinyRPCClient</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> &lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type AnyTRPCRouter = Router<any, any>
import AnyTRPCRouter">AnyTRPCRouter</data-lsp></span><span style="color: #C9D1D9">&gt;(</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF"><data-lsp lsp="function createRecursiveProxy(callback: ProxyCallback, path: readonly string[]): unknown">createRecursiveProxy</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> (</span><span style="color: #FFA657"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const path: string[]">path</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> [</span><span style="color: #FF7B72">...</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>.<data-lsp lsp="(property) ProxyCallbackOptions.path: readonly string[]">path</data-lsp>]; </span><span style="color: #8B949E">// e.g. ["post", "byId", "query"]</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const path: string[]">path</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<string>.pop(): string | undefined">pop</data-lsp></span><span style="color: #C9D1D9">()</span><span style="color: #FF7B72">!</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">|</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'mutate'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const path: string[]">path</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Array<string>.join(separator?: string): string">join</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'.'</span><span style="color: #C9D1D9">); </span><span style="color: #8B949E">// "post.byId" - this is the path procedures have on the backend</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">`${</span><span style="color: #C9D1D9"><data-lsp lsp="(parameter) baseUrl: string">baseUrl</data-lsp></span><span style="color: #A5D6FF">}/${</span><span style="color: #C9D1D9"><data-lsp lsp="const dotPath: string">dotPath</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">;</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> [</span><span style="color: #79C0FF"><data-lsp lsp="const input: unknown">input</data-lsp></span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) opts: ProxyCallbackOptions">opts</data-lsp>.<data-lsp lsp="(property) ProxyCallbackOptions.args: readonly unknown[]">args</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const input: unknown">input</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">&amp;&amp;</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var JSON: JSON">JSON</data-lsp></span><span style="color: #C9D1D9">.</span><span style="color: #79C0FF"><data-lsp lsp="(method) JSON.stringify(value: any, replacer?: (this: any, key: string, value: any) => any, space?: string | number): string (+1 overload)">stringify</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="const input: {} | null">input</data-lsp>);</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">let</span><span style="color: #C9D1D9"> <data-lsp lsp="let body: string | undefined">body</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">undefined</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">|</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">string</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var undefined">undefined</data-lsp></span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (<data-lsp lsp="const stringifiedInput: string | false">stringifiedInput</data-lsp> </span><span style="color: #FF7B72">!==</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">false</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (<data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9">) {</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="let uri: string">uri</data-lsp> </span><span style="color: #FF7B72">+=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">`?input=${</span><span style="color: #79C0FF"><data-lsp lsp="function encodeURIComponent(uriComponent: string | number | boolean): string">encodeURIComponent</data-lsp></span><span style="color: #A5D6FF">(</span><span style="color: #C9D1D9"><data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp></span><span style="color: #A5D6FF">)</span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">      } </span><span style="color: #FF7B72">else</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        <data-lsp lsp="let body: string | undefined">body</data-lsp> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> <data-lsp lsp="const stringifiedInput: string">stringifiedInput</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">      }</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="const json: TRPCResponse">json</data-lsp></span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="(alias) type TRPCResponse<TData = unknown, TError extends TRPCErrorShape = TRPCErrorShape<object>> = TRPCErrorResponse<TError> | TRPCSuccessResponse<TData>
import TRPCResponse">TRPCResponse</data-lsp></span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF"><data-lsp lsp="function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>">fetch</data-lsp></span><span style="color: #C9D1D9">(<data-lsp lsp="let uri: string">uri</data-lsp>, {</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.method?: string | undefined">method</data-lsp>: <data-lsp lsp="const method: &quot;query&quot; | &quot;mutate&quot;">method</data-lsp> </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'query'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'GET'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'POST'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.headers?: HeadersInit | undefined">headers</data-lsp>: {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #A5D6FF">'Content-Type'</span><span style="color: #C9D1D9">: </span><span style="color: #A5D6FF">'application/json'</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">      },</span></div><div class="line"><span style="color: #C9D1D9">      <data-lsp lsp="(property) RequestInit.body?: BodyInit | null | undefined">body</data-lsp>,</span></div><div class="line"><span style="color: #C9D1D9">    }).</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Promise<Response>.then<any, TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>(onfulfilled?: ((value: Response) => any) | null | undefined, onrejected?: ((reason: any) => TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown> | PromiseLike<TRPCErrorResponse<TRPCErrorShape<object>> | TRPCSuccessResponse<unknown>>) | null | undefined): Promise<...>">then</data-lsp></span><span style="color: #C9D1D9">((</span><span style="color: #FFA657"><data-lsp lsp="(parameter) res: Response">res</data-lsp></span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> <data-lsp lsp="(parameter) res: Response">res</data-lsp>.</span><span style="color: #D2A8FF"><data-lsp lsp="(method) Body.json(): Promise<any>">json</data-lsp></span><span style="color: #C9D1D9">());</span></div><div class="line">&nbsp;</div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (</span><span style="color: #A5D6FF">'error'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> <data-lsp lsp="const json: TRPCResponse">json</data-lsp>) {</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">throw</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">new</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF"><data-lsp lsp="var Error: ErrorConstructor
new (message?: string) => Error">Error</data-lsp></span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">`Error: ${</span><span style="color: #C9D1D9"><data-lsp lsp="const json: TRPCErrorResponse<TRPCErrorShape<object>>">json</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) JSONRPC2.ErrorResponse<TRPCErrorShape<object>>.error: TRPCErrorShape<object>">error</data-lsp></span><span style="color: #A5D6FF">.</span><span style="color: #C9D1D9"><data-lsp lsp="(property) TRPCErrorShape<object>.message: string">message</data-lsp></span><span style="color: #A5D6FF">}`</span><span style="color: #C9D1D9">);</span></div><div class="line"><span style="color: #C9D1D9">    }</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// No error - all good. Return the data.</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> <data-lsp lsp="const json: TRPCSuccessResponse<unknown>">json</data-lsp>.<data-lsp lsp="(property) JSONRPC2.ResultResponse<TRPCResult<unknown>>.result: TRPCResult<unknown>">result</data-lsp>.<data-lsp lsp="(property) TRPCResult<unknown>.data: unknown">data</data-lsp>;</span></div><div class="line"><span style="color: #C9D1D9">  }, []) </span><span style="color: #FF7B72">as</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657"><data-lsp lsp="type DecorateRouterRecord<TRecord extends TRPCRouterRecord> = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TRPCRouterRecord ? DecorateRouterRecord<$Value> : $Value extends AnyTRPCProcedure ? DecorateProcedure<$Value> : never : never; }">DecorateRouterRecord</data-lsp></span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657"><data-lsp lsp="(type parameter) TRouter in <TRouter extends AnyTRPCRouter>(baseUrl: string): DecorateRouterRecord<TRouter[&quot;_def&quot;][&quot;record&quot;]>">TRouter</data-lsp></span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #8B949E">//   ^? provide empty array as path to begin with</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Most notably here is that our path is <code>.</code>-separated instead of <code>/</code>. This allows us to have a single API handler on the server which will process all requests, and not one for each procedure. If you're using a framework with filebased routing such as Next.js, you might recognize the catchall <code>/api/trpc/[trpc].ts</code> file which will match all procedure paths.</p>
<p>We also have a <code>TRPCResponse</code> type annotation on the <code>fetch</code>-request. This determines the JSONRPC-compliant response format that the server responds with. You can read more on that <a href="https://trpc.io/docs/rpc" target="_blank" rel="noopener noreferrer">here</a>. TL;DR, we get back either a <code>result</code> or an <code>error</code> object, which we can use to determine if the request was successful or not and do appropriate error handling if something went wrong.</p>
<p>And that's it! This is all the code you'll need to call your tRPC procedures on your client as if they were local functions. On the surface, it looks like we're just calling the <code>publicProcedure.query / mutation</code>'s resolver function via normal property accesses, but we're actually crossing a network boundary so we can use server-side libraries such as Prisma without leaking database credentials.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="trying-it-out">Trying it out!<a href="https://trpc.io/blog/tinyrpc-client#trying-it-out" class="hash-link" aria-label="Direct link to Trying it out!" title="Direct link to Trying it out!">​</a></h2>
<p>Now, create the client and provide it your server's url and you'll get full autocompletion and type safety when you call your procedures!</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">url</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'http://localhost:3000/api/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTinyRPCClient</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;(url);</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// 🧙‍♀️ magic autocompletion</span></div><div class="line"></div><div class="line"><span style="color: #1976D2">client</span><span style="color: #24292EFF">.</span><span style="color: #1976D2">post</span><span style="color: #24292EFF">.b;</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// 👀 fully typesafe</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">post</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">post</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">byId</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">({ id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'123'</span><span style="color: #24292EFF"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">url</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'http://localhost:3000/api/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">client</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTinyRPCClient</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;(url);</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// 🧙‍♀️ magic autocompletion</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">client.post.b;</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// 👀 fully typesafe</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">post</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.post.byId.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">({ id: </span><span style="color: #A5D6FF">'123'</span><span style="color: #C9D1D9"> });</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>The full code for the client can be found <a href="https://github.com/trpc/trpc/blob/main/packages/tests/showcase/tinyrpc.ts" target="_blank" rel="noopener noreferrer">here</a>, and tests showing the usage <a href="https://github.com/trpc/trpc/blob/main/packages/tests/showcase/tinyrpc.test.ts" target="_blank" rel="noopener noreferrer">here</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="conclusion">Conclusion<a href="https://trpc.io/blog/tinyrpc-client#conclusion" class="hash-link" aria-label="Direct link to Conclusion" title="Direct link to Conclusion">​</a></h2>
<p>I hope you enjoyed this article and learned something about how tRPC works. You should probably not use this to in favor of @trpc/client which is only a couple of KBs bigger - it comes with a lot more flexibility than what we're showcasing here:</p>
<ul>
<li>Query options for abort signals, ssr etc...</li>
<li>Links</li>
<li>Procedure batching</li>
<li>WebSockets / subscriptions</li>
<li>Nice error handling</li>
<li>Data transformers</li>
<li>Edge cases handling like when we don't get a tRPC-compliant response</li>
</ul>
<p>We also didn't cover much of the server-side of things today, maybe we'll cover that in a future article. If you have any questions, feel free to bug me on <a href="https://twitter.com/jullerino" target="_blank" rel="noopener noreferrer">Twitter</a>.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[TypeScript performance lessons while refactoring for v10]]></title>
            <link>https://trpc.io/blog/typescript-performance-lessons</link>
            <guid>https://trpc.io/blog/typescript-performance-lessons</guid>
            <pubDate>Sat, 14 Jan 2023 00:00:00 GMT</pubDate>
            <description><![CDATA[As library authors, our goal is to provide the best possible developer experience (DX) for our peers. Reducing time-to-error and providing intuitive APIs removes mental overhead from the minds of developers so that they can focus on what's most important: great end user experience.]]></description>
            <content:encoded><![CDATA[<p>As library authors, our goal is to provide the best possible developer experience (DX) for our peers. Reducing time-to-error and providing intuitive APIs removes mental overhead from the minds of developers so that they can focus on what's most important: great end user experience.</p>
<!-- -->
<p>It's no secret that TypeScript is the driving force behind how tRPC ships its amazing DX. TypeScript adoption is the modern standard in delivering great JavaScript-based experiences today - but this improved certainty around types does have some tradeoffs.</p>
<p>Today, the TypeScript type checker is prone to becoming slow (although releases like <a href="https://devblogs.microsoft.com/typescript/announcing-typescript-4-9/#performance-improvements" target="_blank" rel="noopener noreferrer">TS 4.9</a> are promising!). Libraries almost always contain the fanciest TypeScript incantations in your codebase, pressing your TS compiler to its limits. For this reason, library authors like us must be mindful of our contributions to that burden and do our best to keep your IDE working as fast as possible.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="automating-library-performance">Automating library performance<a href="https://trpc.io/blog/typescript-performance-lessons#automating-library-performance" class="hash-link" aria-label="Direct link to Automating library performance" title="Direct link to Automating library performance">​</a></h2>
<p>While tRPC was in <code>v9</code>, we began seeing reports from developers that their large tRPC routers were starting to have detrimental effects on their type checker. This was a new experience for tRPC as we saw tremendous adoption during the <code>v9</code> phase of tRPC's development. With more developers creating larger and larger products with tRPC, some cracks began to show.</p>
<p>Your library may not be slow <em>now</em>, but it's important to keep an eye on performance as your library grows and changes. Automated testing can remove an immense burden from your library authoring (and application building!) by <em>programmatically</em> testing your library code on each commit.</p>
<p>For tRPC, we do our best to ensure this by <a href="https://github.com/trpc/trpc/blob/9fc2d06a8924da73e10b9d4497f3a1f53de706ed/scripts/generateBigBoi.ts" target="_blank" rel="noopener noreferrer">generating</a> and <a href="https://github.com/trpc/trpc/blob/9fc2d06a8924da73e10b9d4497f3a1f53de706ed/packages/tests/server/react/bigBoi.test.tsx" target="_blank" rel="noopener noreferrer">testing</a> a router with 3,500 procedures and 1,000 routers. But this only tests how far we can push the TS compiler before it breaks and not how long type-checking takes. We test all three pieces of the library (server, vanilla client, and the React client) because they all have different code paths. In the past, we have seen regressions that are isolated to one section of the library and rely on our tests to show us when those unexpected behaviors occur. (We still do <a href="https://github.com/trpc/trpc/issues/2892" target="_blank" rel="noopener noreferrer">want to do more</a> to measure compilation times)</p>
<p>tRPC is not a runtime-heavy library so our performance metrics are centered around type-checking. Therefore, we stay mindful of:</p>
<ul>
<li>Being slow to type-check using <code>tsc</code></li>
<li>Having a large initial load time</li>
<li>If the TypeScript language server takes a long time to respond to changes</li>
</ul>
<p>The last point is one that the tRPC must pay attention to the most. You <strong>never</strong> want to your developers to have to wait for the language server to update after a change. This is where tRPC <strong>must</strong> maintain performance so that you can enjoy great DX.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="how-i-found-performance-opportunities-in-trpc">How I found performance opportunities in tRPC<a href="https://trpc.io/blog/typescript-performance-lessons#how-i-found-performance-opportunities-in-trpc" class="hash-link" aria-label="Direct link to How I found performance opportunities in tRPC" title="Direct link to How I found performance opportunities in tRPC">​</a></h2>
<p>There is always a tradeoff between TypeScript accuracy and compiler performance. Both are important concerns for other developers so we must be extremely conscious of how we write types. Will it be possible for an application to run into severe errors because a certain type is "too loose"? Is the performance gain worth it?</p>
<p>Is there even going to be a meaningful performance gain at all? Great question.</p>
<p>Let's have a look at <em>how</em> to find moments for performance improvements in TypeScript code. We'll visit the process I went through to create <a href="https://github.com/trpc/trpc/pull/2716" target="_blank" rel="noopener noreferrer">PR #2716</a>, resulting in a 59% decrease in TS compilation time.</p>
<hr>
<p>TypeScript has a built-in <a href="https://github.com/microsoft/TypeScript/wiki/Performance-Tracing" target="_blank" rel="noopener noreferrer">tracing tool</a> that can help you find the bottleneck in your types. It's not perfect, but it's the best tool available.</p>
<p>It's ideal to test your library on a real-world app to simulate what your library is doing for real developers. For tRPC, I created a basic <a href="https://create.t3.gg/" target="_blank" rel="noopener noreferrer">T3 app</a> resembling what many of our users work with.</p>
<p>Here's the steps I followed to trace tRPC:</p>
<ol>
<li>
<p><a href="https://docs.npmjs.com/cli/commands/npm-link" target="_blank" rel="noopener noreferrer">Locally link the library</a> to the example app. This is so you can change your library code and immediately test changes locally.</p>
</li>
<li>
<p>Run this command in the example app:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">sh</div><div class="code-container"><code><div class="line"><span style="color: #24292EFF">tsc --generateTrace ./trace --incremental </span><span style="color: #6F42C1">false</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">sh</div><div class="code-container"><code><div class="line"><span style="color: #C9D1D9">tsc --generateTrace ./trace --incremental </span><span style="color: #79C0FF">false</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
</li>
<li>
<p>You'll be given a <code>trace/trace.json</code> file on your machine. You can open that file in a trace analysis app (I use <a href="https://ui.perfetto.dev/" target="_blank" rel="noopener noreferrer">Perfetto</a>) or <code>chrome://tracing</code>.</p>
</li>
</ol>
<p>This is where things get interesting and we can start to learn about the performance profile of the types in the application. Here's what the first trace looked like:
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-1.png" alt="trace bar showing that src/pages/index.ts took 332ms to type-check" class="img_Njog"></p>
<p>A longer bar means more time spent performing that process. I've selected the top green bar for this screenshot, indicating that <code>src/pages/index.ts</code> is the bottleneck. Under the <code>Duration</code> field, you'll see that it took 332ms - an enormous amount of time to spend type-checking! The blue <code>checkVariableDeclaration</code> bar tells us the compiler spent most of its time on one variable.
Clicking on that bar will tell us which one it is:
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-2.png" alt="trace info showing the variable's position is 275" class="img_Njog">
The <code>pos</code> field reveals the position of the variable in the file's text. Going to that position in <code>src/pages/index.ts</code> reveals that the culprit is <code>utils = trpc.useContext()</code>!</p>
<p>But how could this be? We're just using a simple hook! Let's look at the code:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { AppRouter } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'~/server/trpc'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTRPCReact</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;();</span></div><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Home</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">NextPage</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> () </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">r0</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">greeting</span><span style="color: #6F42C1">.useQuery</span><span style="color: #24292EFF">({ who</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'from tRPC'</span><span style="color: #24292EFF"> });</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">utils</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useContext</span><span style="color: #24292EFF">();</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2">utils</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">r49</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">greeting</span><span style="color: #6F42C1">.invalidate</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">default</span><span style="color: #24292EFF"> Home;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { AppRouter } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'~/server/trpc'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">trpc</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTRPCReact</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;();</span></div><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">Home</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">NextPage</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> () </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.r0.greeting.</span><span style="color: #D2A8FF">useQuery</span><span style="color: #C9D1D9">({ who: </span><span style="color: #A5D6FF">'from tRPC'</span><span style="color: #C9D1D9"> });</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">utils</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useContext</span><span style="color: #C9D1D9">();</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  utils.r49.greeting.</span><span style="color: #D2A8FF">invalidate</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #FFA657"> </span><span style="color: #FF7B72">default</span><span style="color: #FFA657"> </span><span style="color: #C9D1D9">Home;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Okay, not much to see here. We only see a single <code>useContext</code> and a query invalidation. Nothing that <em>should be</em> TypeScript heavy at face value, indicating that the problem must be deeper in the stack. Let's look at the types behind this variable:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecorateProcedure</span><span style="color: #24292EFF">&lt;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Procedure</span><span style="color: #24292EFF">&lt;</span><span style="color: #1976D2">any</span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyQueryProcedure</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">/**</span></div><div class="line"><span style="color: #C2C3C5">   * </span><span style="color: #D32F2F">@see</span><span style="color: #C2C3C5"> https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation</span></div><div class="line"><span style="color: #C2C3C5">   */</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">invalidate</span><span style="color: #24292EFF">(</span></div><div class="line"><span style="color: #24292EFF">    input</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">inferProcedureInput</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedure</span><span style="color: #24292EFF">&gt;</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    filters</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">InvalidateQueryFilters</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    options</span><span style="color: #D32F2F">?:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">InvalidateOptions</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  )</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">Promise</span><span style="color: #24292EFF">&lt;</span><span style="color: #1976D2">void</span><span style="color: #24292EFF">&gt;;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// ... and so on for all the other React Query utilities</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecoratedProcedureUtilsRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">OmitNeverKeys</span><span style="color: #24292EFF">&lt;{</span></div><div class="line"><span style="color: #24292EFF">    [</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">]]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">LegacyV9ProcedureTag</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyRouter</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecoratedProcedureUtilsRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]&gt;</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">] </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AnyQueryProcedure</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #D32F2F">?</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">DecorateProcedure</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TRouter</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TRouter</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'_def'</span><span style="color: #24292EFF">][</span><span style="color: #22863A">'record'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]&gt;</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">never</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">  }&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecorateProcedure</span><span style="color: #C9D1D9">&lt;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">Procedure</span><span style="color: #C9D1D9">&lt;</span><span style="color: #79C0FF">any</span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyQueryProcedure</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">/**</span></div><div class="line"><span style="color: #8B949E">   * </span><span style="color: #FF7B72">@see</span><span style="color: #8B949E"> </span><span style="color: #C9D1D9">https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation</span></div><div class="line"><span style="color: #8B949E">   */</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF">invalidate</span><span style="color: #C9D1D9">(</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">input</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">inferProcedureInput</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedure</span><span style="color: #C9D1D9">&gt;,</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">filters</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">InvalidateQueryFilters</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">options</span><span style="color: #FF7B72">?:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">InvalidateOptions</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  )</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">Promise</span><span style="color: #C9D1D9">&lt;</span><span style="color: #79C0FF">void</span><span style="color: #C9D1D9">&gt;;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// ... and so on for all the other React Query utilities</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecoratedProcedureUtilsRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">OmitNeverKeys</span><span style="color: #C9D1D9">&lt;{</span></div><div class="line"><span style="color: #C9D1D9">    [</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">]]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">LegacyV9ProcedureTag</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyRouter</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecoratedProcedureUtilsRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]&gt;</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">] </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AnyQueryProcedure</span></div><div class="line"><span style="color: #C9D1D9">          </span><span style="color: #FF7B72">?</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">DecorateProcedure</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">, </span><span style="color: #FFA657">TRouter</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'_def'</span><span style="color: #C9D1D9">][</span><span style="color: #A5D6FF">'record'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]&gt;</span></div><div class="line"><span style="color: #C9D1D9">          </span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">never</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">  }&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Okay, now we have some things to unpack and learn about. Let's figure out what this code is doing first.</p>
<p>We have a recursive type <code>DecoratedProcedureUtilsRecord</code> that walks through all the procedures in the router and "decorates" (adds methods to) them with React Query utilities like <a href="https://tanstack.com/query/v4/docs/framework/react/guides/query-invalidation" target="_blank" rel="noopener noreferrer"><code>invalidateQueries</code></a>.</p>
<p>In tRPC v10 we still support old <code>v9</code> routers, but <code>v10</code> clients cannot call procedures from <code>v9</code> routers. So for each procedure we check if it's a <code>v9</code> procedure (<code>extends LegacyV9ProcedureTag</code>) and strip it out if so. It's all a lot of work for TypeScript to do...<strong>if it's not lazily evaluated</strong>.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="lazy-evaluation">Lazy evaluation<a href="https://trpc.io/blog/typescript-performance-lessons#lazy-evaluation" class="hash-link" aria-label="Direct link to Lazy evaluation" title="Direct link to Lazy evaluation">​</a></h3>
<p>The problem here is that TypeScript is evaluating all of this code in the type system, even though it's not used immediately. Our code is only using <code>utils.r49.greeting.invalidate</code> so TypeScript should only need to unwrap the <code>r49</code> property (a router), then the <code>greeting</code> property (a procedure), and finally the <code>invalidate</code> function for that procedure. No other types are needed in that code and immediately finding the type for every React Query utility method for all your tRPC procedures would unnecessarily slow TypeScript down. TypeScript defers type evaluation of properties on <strong>objects</strong> until they are directly used, so theoretically our type above should get lazy evaluation...right?</p>
<p>Well, it's not <em>exactly</em> an object. There's actually a type wrapping the entire thing: <code>OmitNeverKeys</code>. This type is a utility that removes keys that have the value <code>never</code> from an object. This is the part where we strip off the v9 procedures so those properties don't show up in Intellisense.</p>
<p>But this creates a huge performance issue. We forced TypeScript to evaluate the values of <strong>all</strong> types now to check if they are <code>never</code>.</p>
<p>How can we fix this? Let's change our types to <em>do less</em>.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="get-lazy">Get lazy<a href="https://trpc.io/blog/typescript-performance-lessons#get-lazy" class="hash-link" aria-label="Direct link to Get lazy" title="Direct link to Get lazy">​</a></h3>
<p>We need to find a way for the <code>v10</code> API to adapt to the legacy <code>v9</code> routers more gracefully. New tRPC projects should not suffer from the reduced TypeScript performance of <a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10#using-interop">interop mode</a>.</p>
<p>The idea is to rearrange the core types themselves. <code>v9</code> procedures are different entities than <code>v10</code> procedures so they shouldn't share the same space in our library code. On the tRPC server side, this means we had some work to do to store the types on different fields in the router instead of a single <code>record</code> field (see the <code>DecoratedProcedureUtilsRecord</code> from above).</p>
<p>We made a change so <code>v9</code> routers inject their procedures into a <code>legacy</code> field when they are converted to <code>v10</code> routers.</p>
<p>Old types:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  record</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #C2C3C5">// convert a v9 interop router to a v10 router</span></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateV9Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V9Router</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;{</span></div><div class="line"><span style="color: #24292EFF">  [</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">in</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">keyof</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'procedures'</span><span style="color: #24292EFF">]]</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedure</span><span style="color: #24292EFF">&lt;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'procedures'</span><span style="color: #24292EFF">][</span><span style="color: #6F42C1">TKey</span><span style="color: #24292EFF">]</span></div><div class="line"><span style="color: #24292EFF">  &gt; </span><span style="color: #D32F2F">&amp;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #6F42C1">LegacyV9ProcedureTag</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">record</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #8B949E">// convert a v9 interop router to a v10 router</span></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateV9Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V9Router</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;{</span></div><div class="line"><span style="color: #C9D1D9">  [</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">in</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">keyof</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'procedures'</span><span style="color: #C9D1D9">]]</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedure</span><span style="color: #C9D1D9">&lt;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'procedures'</span><span style="color: #C9D1D9">][</span><span style="color: #FFA657">TKey</span><span style="color: #C9D1D9">]</span></div><div class="line"><span style="color: #C9D1D9">  &gt; </span><span style="color: #FF7B72">&amp;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">LegacyV9ProcedureTag</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>If you recall the <code>DecoratedProcedureUtilsRecord</code> type above, you can see that we attached <code>LegacyV9ProcedureTag</code> here to differentiate between <code>v9</code> and <code>v10</code> procedures on the type level and enforce that <code>v9</code> procedures are not called from <code>v10</code> clients.</p>
<p>New types:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  record</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">TProcedureRecord</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// by default, no legacy procedures</span></div><div class="line"><span style="color: #24292EFF">  legacy</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {};</span></div><div class="line"><span style="color: #24292EFF">};</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateV9Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">extends</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V9Router</span><span style="color: #24292EFF">&gt; </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #C2C3C5">// v9 routers inject their procedures into a `legacy` field</span></div><div class="line"><span style="color: #24292EFF">  legacy</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #C2C3C5">// v9 clients require that we filter queries, mutations, subscriptions at the top-level</span></div><div class="line"><span style="color: #24292EFF">    queries</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'queries'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">    mutations</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'mutations'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">    subscriptions</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">MigrateProcedureRecord</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">TV9Router</span><span style="color: #24292EFF">[</span><span style="color: #22863A">'subscriptions'</span><span style="color: #24292EFF">]&gt;;</span></div><div class="line"><span style="color: #24292EFF">  };</span></div><div class="line"><span style="color: #24292EFF">} </span><span style="color: #D32F2F">&amp;</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">V10Router</span><span style="color: #24292EFF">&lt;</span><span style="color: #C2C3C5">/* empty object, v9 routers have no v10 procedures to pass */</span><span style="color: #24292EFF"> {}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">ts</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">record</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">TProcedureRecord</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// by default, no legacy procedures</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">legacy</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> {};</span></div><div class="line"><span style="color: #C9D1D9">};</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateV9Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">extends</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V9Router</span><span style="color: #C9D1D9">&gt; </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #8B949E">// v9 routers inject their procedures into a `legacy` field</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FFA657">legacy</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #8B949E">// v9 clients require that we filter queries, mutations, subscriptions at the top-level</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">queries</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'queries'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">mutations</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'mutations'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FFA657">subscriptions</span><span style="color: #FF7B72">:</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">MigrateProcedureRecord</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">TV9Router</span><span style="color: #C9D1D9">[</span><span style="color: #A5D6FF">'subscriptions'</span><span style="color: #C9D1D9">]&gt;;</span></div><div class="line"><span style="color: #C9D1D9">  };</span></div><div class="line"><span style="color: #C9D1D9">} </span><span style="color: #FF7B72">&amp;</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">V10Router</span><span style="color: #C9D1D9">&lt;</span><span style="color: #8B949E">/* empty object, v9 routers have no v10 procedures to pass */</span><span style="color: #C9D1D9"> {}&gt;;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>Now, we can remove <code>OmitNeverKeys</code> because the procedures are pre-sorted so a router's <code>record</code> property type will contain all the <code>v10</code> procedures and its <code>legacy</code> property type will contain all the <code>v9</code> procedures. We no longer force TypeScript to fully evaluate the huge <code>DecoratedProcedureUtilsRecord</code> type. We can also remove the filtering for<code>v9</code>procedures with <code>LegacyV9ProcedureTag</code>.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="did-it-work">Did it work?<a href="https://trpc.io/blog/typescript-performance-lessons#did-it-work" class="hash-link" aria-label="Direct link to Did it work?" title="Direct link to Did it work?">​</a></h2>
<p>Our new trace shows that the bottleneck has been removed:
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/blog/2023-01-14-typescript-performance-lessons/trace-3.png" alt="trace bar showing that src/pages/index.ts took 136ms to type-check" class="img_Njog"></p>
<p>A substantial improvement! Type-checking time went from 332ms to 136ms 🤯! This may not seem like much in the big picture but it's a huge win. 200ms is a small amount once - but think about:</p>
<ul>
<li>how many other TS libraries are in a project</li>
<li>how many developers are using tRPC today</li>
<li>how many times their types re-evaluate in a work session</li>
</ul>
<p>That's a lot of 200ms adding up to a very big number.</p>
<p>We're always looking for more opportunities to improve the experience of TypeScript developers, whether it's with tRPC or a TS-based problem to solve in another project. @ me on <a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">Twitter</a> if you want to talk TypeScript.</p>
<p>Thanks to <a href="https://twitter.com/anthonysheww" target="_blank" rel="noopener noreferrer">Anthony Shew</a> for helping write this post and to <a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">Alex</a> for reviewing!</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Announcing tRPC v10]]></title>
            <link>https://trpc.io/blog/announcing-trpc-10</link>
            <guid>https://trpc.io/blog/announcing-trpc-10</guid>
            <pubDate>Mon, 21 Nov 2022 00:00:00 GMT</pubDate>
            <description><![CDATA[tRPC provides a great developer experience by enforcing tight, full-stack type bindings through the power of TypeScript. No API contract drift, no code generation.]]></description>
            <content:encoded><![CDATA[<p>tRPC provides a great developer experience by enforcing tight, full-stack type bindings through the power of TypeScript. No API contract drift, no code generation.</p>
<!-- -->
<p>Since our last major version release in August 2021, the tRPC community has seen substantial growth:</p>
<ul>
<li>We now have over <a href="https://github.com/trpc/trpc" target="_blank" rel="noopener noreferrer">15,000 stars on GitHub</a></li>
<li><a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">A Discord community</a> with over 2,000 members</li>
<li><a href="https://www.npmjs.com/package/@trpc/server" target="_blank" rel="noopener noreferrer">100k+ weekly npm downloads</a></li>
<li><a href="https://github.com/trpc/trpc/graphs/contributors" target="_blank" rel="noopener noreferrer">Nearly 200 contributors</a></li>
<li><a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">A growing ecosystem of extensions, examples, and content</a></li>
</ul>
<p><strong>Today, we're launching tRPC v10</strong>. We're excited to share that v10 is already being used in production by many large TypeScript projects. This official release announces general availability to the wider community.</p>
<p>For new projects, you can get up and running with an <a href="https://trpc.io/awesome#-starting-points-example-projects-etc" target="_blank" rel="noopener noreferrer">example application</a> to learn about tRPC v10. For projects that were already enjoying tRPC v9, <a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">visit the v10 migration guide</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="overview-of-changes">Overview of changes<a href="https://trpc.io/blog/announcing-trpc-10#overview-of-changes" class="hash-link" aria-label="Direct link to Overview of changes" title="Direct link to Overview of changes">​</a></h2>
<p>v10 is tRPC's biggest release ever. This is the first time we've made any fundamental changes to the structure of tRPC and we believe these changes unlock new possibilities for fast-moving teams working on cutting edge applications.</p>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="improved-developer-experience">Improved developer experience<a href="https://trpc.io/blog/announcing-trpc-10#improved-developer-experience" class="hash-link" aria-label="Direct link to Improved developer experience" title="Direct link to Improved developer experience">​</a></h3>
<p>tRPC v10 embraces your IDE. We want to unify your types - but we've also brought together your frontend, backend, and editing experience in this version.</p>
<p>With v10, you can:</p>
<ul>
<li>Use <em>"Go to Definition"</em> to jump straight from your frontend consumer to your backend procedure</li>
<li>Use <em>"Rename Symbol"</em> to give a new name to an input argument or procedure across your whole application</li>
<li><a href="https://trpc.io/docs/v10/infer-types" target="_blank" rel="noopener noreferrer">Infer types more easily</a> for when you'd like to use your tRPC types in your application manually</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="powerful-backend-framework">Powerful backend framework<a href="https://trpc.io/blog/announcing-trpc-10#powerful-backend-framework" class="hash-link" aria-label="Direct link to Powerful backend framework" title="Direct link to Powerful backend framework">​</a></h3>
<p>In v10, we've revisited the syntax for how you define your backend procedures, opening up more opportunities to bring in your desired logic in healthy ways. This version of tRPC features:</p>
<ul>
<li><a href="https://trpc.io/docs/v10/middlewares" target="_blank" rel="noopener noreferrer">Reusable middlewares</a> with <a href="https://trpc.io/docs/v10/middlewares#context-extension" target="_blank" rel="noopener noreferrer">Context Extension</a></li>
<li><a href="https://trpc.io/docs/v10/procedures#reusable-base-publicprocedures" target="_blank" rel="noopener noreferrer">Chainable &amp; reusable procedures</a> with the ability to use <a href="https://trpc.io/docs/v10/procedures#multiple-input-parsers" target="_blank" rel="noopener noreferrer">multiple input parsers</a></li>
<li>Flexible <a href="https://trpc.io/docs/v10/error-handling" target="_blank" rel="noopener noreferrer">error handling</a> with <a href="https://trpc.io/docs/v10/error-formatting" target="_blank" rel="noopener noreferrer">custom error formatting</a></li>
<li><a href="https://trpc.io/docs/v10/metadata" target="_blank" rel="noopener noreferrer">Procedure metadata</a> to decorate your procedures with more information</li>
</ul>
<h3 class="anchor anchorWithStickyNavbar_MYIC" id="massively-improved-typescript-performance">Massively improved TypeScript performance<a href="https://trpc.io/blog/announcing-trpc-10#massively-improved-typescript-performance" class="hash-link" aria-label="Direct link to Massively improved TypeScript performance" title="Direct link to Massively improved TypeScript performance">​</a></h3>
<p>TypeScript enables developers to do incredible things - but it can come at a cost. Many of the techniques we use to keep your types tight are heavy work on the TypeScript compiler. We heard community feedback that the largest applications using tRPC v9 were beginning to suffer from decreased performance in developers' IDEs as a result of this compiler pressure.</p>
<p>Our goal is to enhance the developer experience for applications of all sizes. In v10, we've dramatically improved TypeScript performance (especially with TS incremental compilation) so that your editor stays snappy.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="incremental-migration">Incremental migration<a href="https://trpc.io/blog/announcing-trpc-10#incremental-migration" class="hash-link" aria-label="Direct link to Incremental migration" title="Direct link to Incremental migration">​</a></h2>
<p>We've also put in a lot of work to make the migration experience as straightforward as possible, including an <code>interop()</code> method that allows (almost) full backward compatibility with v9 routers. <a href="https://trpc.io/docs/v10/migrate-from-v9-to-v10" target="_blank" rel="noopener noreferrer">Visit the migration guide</a> for more information.</p>
<p><a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">Sachin</a> from the core team has also made <a href="https://github.com/sachinraja/trpc-v10-migrate-codemod" target="_blank" rel="noopener noreferrer">a codemod</a> that can do much of the heavy lifting of the migration for you.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="a-growing-ecosystem">A growing ecosystem<a href="https://trpc.io/blog/announcing-trpc-10#a-growing-ecosystem" class="hash-link" aria-label="Direct link to A growing ecosystem" title="Direct link to A growing ecosystem">​</a></h2>
<p>A rich set of sub-libraries is continuing to form around tRPC. Here are a few examples:</p>
<ul>
<li><a href="https://github.com/jlalmes/trpc-openapi" target="_blank" rel="noopener noreferrer">trpc-openapi</a> to easily create REST-compatible endpoints</li>
<li><a href="https://github.com/t3-oss/create-t3-app" target="_blank" rel="noopener noreferrer">create-t3-app</a> to bootstrap a full-stack Next.js application with tRPC</li>
<li><a href="https://github.com/t3-oss/create-t3-turbo" target="_blank" rel="noopener noreferrer">create-t3-turbo</a> to kickstart your next React Native app with tRPC</li>
<li><a href="https://github.com/jlalmes/trpc-chrome" target="_blank" rel="noopener noreferrer">trpc-chrome</a> for building Chrome extensions using tRPC</li>
<li>Adapters for frameworks like <a href="https://github.com/OrJDev/solid-trpc" target="_blank" rel="noopener noreferrer">Solid</a>, <a href="https://github.com/icflorescu/trpc-sveltekit-example" target="_blank" rel="noopener noreferrer">Svelte</a>, and <a href="https://github.com/michealroberts/usetrpc" target="_blank" rel="noopener noreferrer">Vue</a></li>
</ul>
<p>For more plugins, examples, and adapters, <a href="https://trpc.io/awesome" target="_blank" rel="noopener noreferrer">visit the Awesome tRPC collection</a>.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="thank-you">Thank you!<a href="https://trpc.io/blog/announcing-trpc-10#thank-you" class="hash-link" aria-label="Direct link to Thank you!" title="Direct link to Thank you!">​</a></h2>
<p>The core team and I want you to know: we're just getting started. We're already busy experimenting with <a href="https://github.com/reactjs/rfcs/pull/229" target="_blank" rel="noopener noreferrer">React Server Components</a> and Next.js 13.</p>
<p>I also want to give a huuuge shoutout to <a href="https://twitter.com/s4chinraja" target="_blank" rel="noopener noreferrer">Sachin</a>, <a href="https://twitter.com/jullerino" target="_blank" rel="noopener noreferrer">Julius</a>, <a href="https://twitter.com/jlalmes" target="_blank" rel="noopener noreferrer">James</a>, <a href="https://twitter.com/ixahmedxii" target="_blank" rel="noopener noreferrer">Ahmed</a>, <a href="https://twitter.com/trashh_dev" target="_blank" rel="noopener noreferrer">Chris</a>, <a href="https://twitter.com/theo" target="_blank" rel="noopener noreferrer">Theo</a>, <a href="https://twitter.com/shewDev" target="_blank" rel="noopener noreferrer">Anthony</a>, and <a href="https://github.com/trpc/trpc#all-contributors" target="_blank" rel="noopener noreferrer">all the contributors who helped make this release possible</a>.</p>
<p>Thanks for using and supporting tRPC.</p>
<hr>
<ul>
<li>Follow <a href="https://twitter.com/trpcio" target="_blank" rel="noopener noreferrer">@trpcio</a> on Twitter.</li>
<li>Join our <a href="https://trpc.io/discord" target="_blank" rel="noopener noreferrer">Discord-community</a></li>
<li><a href="https://trpc.io/#try-it-out" target="_blank" rel="noopener noreferrer">Try out tRPC in your browser</a></li>
</ul>
<a id="sponsor-button" href="https://trpc.io/sponsor" class="group flex h-12 w-max items-center gap-4 rounded-lg border-2 bg-zinc-200 px-4 py-2 transition hover:bg-zinc-100 dark:border-zinc-900 dark:bg-zinc-800 hover:dark:border-zinc-700 hover:dark:bg-zinc-900"><svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="aspect-square h-6 fill-pink-500 transition-transform duration-200 ease-in group-hover:scale-110"><path d="M17.625 1.499c-2.32 0-4.354 1.203-5.625 3.03-1.271-1.827-3.305-3.03-5.625-3.03C3.129 1.499 0 4.253 0 8.249c0 4.275 3.068 7.847 5.828 10.227a33.14 33.14 0 0 0 5.616 3.876l.028.017.008.003-.001.003c.163.085.342.126.521.125.179.001.358-.041.521-.125l-.001-.003.008-.003.028-.017a33.14 33.14 0 0 0 5.616-3.876C20.932 16.096 24 12.524 24 8.249c0-3.996-3.129-6.75-6.375-6.75zm-.919 15.275a30.766 30.766 0 0 1-4.703 3.316l-.004-.002-.004.002a30.955 30.955 0 0 1-4.703-3.316c-2.677-2.307-5.047-5.298-5.047-8.523 0-2.754 2.121-4.5 4.125-4.5 2.06 0 3.914 1.479 4.544 3.684.143.495.596.797 1.086.796.49.001.943-.302 1.085-.796.63-2.205 2.484-3.684 4.544-3.684 2.004 0 4.125 1.746 4.125 4.5 0 3.225-2.37 6.216-5.048 8.523z"></path></svg><span class="font-semibold text-zinc-900 no-underline dark:text-zinc-300">Become a Sponsor!</span></a>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Introducing tRPC]]></title>
            <link>https://trpc.io/blog/introducing-trpc</link>
            <guid>https://trpc.io/blog/introducing-trpc</guid>
            <pubDate>Wed, 05 May 2021 00:00:00 GMT</pubDate>
            <description><![CDATA[tRPC gives you end-to-end type safety from your (node-)server to your client, without even declaring types. All you do on the backend is that you return data in a function and on the frontend you use said data based on the endpoint name.]]></description>
            <content:encoded><![CDATA[<p>tRPC gives you end-to-end type safety from your (node-)server to your client, <em>without even declaring types</em>. All you do on the backend is that you return data in a function and on the frontend you use said data based on the endpoint name.</p>
<!-- -->
<p>👋 I'm Alex, or "KATT" on GitHub, and I want to tell you about a library called <a href="https://trpc.io/" target="_blank" rel="noopener noreferrer">tRPC</a>. I've not published any articles about it yet, so I'm just writing this intro to get the ball rolling (but we have already somehow reached &gt;530 🌟 on GitHub). Expect articles &amp; video intros to come! If you want to stay up-to-date or want to ask questions, you can follow me on Twitter at <a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">@alexdotjs</a>.</p>
<p><strong>This is how it can look like when doing a tRPC endpoint &amp; client call:</strong>
<img decoding="async" loading="lazy" src="https://assets.trpc.io/www/v9/trpcgif.gif" alt="Alt Text" class="img_Njog"></p>
<p>I have made a library for React (<code>@trpc/react</code>) that sits on top of the great react-query, but the client library (<code>@trpc/client</code>) works without React (if you want to build a specific Svelte/Vue/Angular/[..] lib, please reach out!)</p>
<p>There's no code generation involved &amp; you can pretty easily add it to your existing Next.js/CRA/Express project.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="example">Example<a href="https://trpc.io/blog/introducing-trpc#example" class="hash-link" aria-label="Direct link to Example" title="Direct link to Example">​</a></h2>
<p>Here's an example of a tRPC procedure (aka endpoint) called <code>hello</code>&nbsp;that takes a <code>string</code> argument.</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">appRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.router</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  input</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #6F42C1">resolve</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> ({ input }) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">      text</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`hello </span><span style="color: #D32F2F">${</span><span style="color: #24292EFF">input </span><span style="color: #D32F2F">??</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'world'</span><span style="color: #D32F2F">}</span><span style="color: #22863A">`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    };</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">export</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">typeof</span><span style="color: #24292EFF"> appRouter;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">appRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">router</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, {</span></div><div class="line"><span style="color: #C9D1D9">  input: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #D2A8FF">resolve</span><span style="color: #C9D1D9">: ({ </span><span style="color: #FFA657">input</span><span style="color: #C9D1D9"> }) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">      text: </span><span style="color: #A5D6FF">`hello ${</span><span style="color: #C9D1D9">input</span><span style="color: #A5D6FF"> </span><span style="color: #FF7B72">??</span><span style="color: #A5D6FF"> </span><span style="color: #A5D6FF">'world'}`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">    };</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">export</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> </span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">typeof</span><span style="color: #C9D1D9"> appRouter;</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>And here's a type safe client using said data:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">import</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">type</span><span style="color: #24292EFF"> { AppRouter } </span><span style="color: #D32F2F">from</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'./server'</span><span style="color: #24292EFF">;</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">function</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">main</span><span style="color: #24292EFF">() {</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createTRPCClient</span><span style="color: #24292EFF">&lt;</span><span style="color: #6F42C1">AppRouter</span><span style="color: #24292EFF">&gt;({</span></div><div class="line"><span style="color: #24292EFF">    url</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #22863A">`http://localhost:2022`</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  });</span></div><div class="line"></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">result</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">client</span><span style="color: #6F42C1">.query</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@alexdotjs'</span><span style="color: #24292EFF">);</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #1976D2">console</span><span style="color: #6F42C1">.log</span><span style="color: #24292EFF">(result); </span><span style="color: #C2C3C5">// --&gt; { text: "hello @alexdotjs" }</span></div><div class="line"><span style="color: #24292EFF">}</span></div><div class="line"></div><div class="line"><span style="color: #6F42C1">main</span><span style="color: #24292EFF">();</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">import</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">type</span><span style="color: #C9D1D9"> { AppRouter } </span><span style="color: #FF7B72">from</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'./server'</span><span style="color: #C9D1D9">;</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">function</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">main</span><span style="color: #C9D1D9">() {</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">client</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createTRPCClient</span><span style="color: #C9D1D9">&lt;</span><span style="color: #FFA657">AppRouter</span><span style="color: #C9D1D9">&gt;({</span></div><div class="line"><span style="color: #C9D1D9">    url: </span><span style="color: #A5D6FF">`http://localhost:2022`</span><span style="color: #C9D1D9">,</span></div><div class="line"><span style="color: #C9D1D9">  });</span></div><div class="line"></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">result</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> client.</span><span style="color: #D2A8FF">query</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, </span><span style="color: #A5D6FF">'@alexdotjs'</span><span style="color: #C9D1D9">);</span></div><div class="line"><span style="color: #C9D1D9">  console.</span><span style="color: #D2A8FF">log</span><span style="color: #C9D1D9">(result); </span><span style="color: #8B949E">// --&gt; { text: "hello @alexdotjs" }</span></div><div class="line"><span style="color: #C9D1D9">}</span></div><div class="line"></div><div class="line"><span style="color: #D2A8FF">main</span><span style="color: #C9D1D9">();</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p><strong>That's all you need to get type safety!</strong> The <code>result</code> is type inferred from what the backend returns in the function. The data from input is also inferred from the return of the validator, so the data is safe to use straight up - actually, you <em>have to</em> pass the input data through a validator (&amp; tRPC works out-of-the-box with zod/yup/custom validators).</p>
<p>Here's a CodeSandbox link where you can play with the example above: <a href="https://githubbox.com/trpc/trpc/tree/main/examples/standalone-server" target="_blank" rel="noopener noreferrer">https://githubbox.com/trpc/trpc/tree/main/examples/standalone-server</a> (have a look at the terminal output rather than the preview!)</p>
<p><strong><em>Wat? I'm importing code from my backend to my client?</em> - No, you're actually not</strong></p>
<p>Even though it might look like it, no code is shared from the server to the client; TypeScript's <code>import type</code> "[..] only imports declarations to be used for type annotations and declarations. It always gets fully erased, so there’s no remnant of it at runtime." - a feature added in TypeScript 3.8 - <a href="https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#:~:text=import%20type%20only%20imports%20declarations,also%20erased%20from%20TypeScript's%20output." target="_blank" rel="noopener noreferrer">see TypeScript docs</a>.</p>
<p>There's no code generation involved, you can this to your app today as long as you have a way to share types from the server to the client (hopefully you're using a monorepo already).</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="but-were-only-getting-started">But we're only getting started!<a href="https://trpc.io/blog/introducing-trpc#but-were-only-getting-started" class="hash-link" aria-label="Direct link to But we're only getting started!" title="Direct link to But we're only getting started!">​</a></h2>
<p>I mentioned before that there's a React-library, the way to use the data above in React you do:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useQuery</span><span style="color: #24292EFF">([</span><span style="color: #22863A">'hello'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'@alexdotjs'</span><span style="color: #24292EFF">]);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useQuery</span><span style="color: #C9D1D9">([</span><span style="color: #A5D6FF">'hello'</span><span style="color: #C9D1D9">, </span><span style="color: #A5D6FF">'@alexdotjs'</span><span style="color: #C9D1D9">]);</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>.. and you'll get type safe data on the client.</p>
<p>You can add tRPC today with your existing brownfield project (got adapters for Express/Next.js) &amp; it works fine with CRA and should work with React Native as well. It is not even tied to React, so if you want to do a Svelte or Vue lib, please get in touch with me.</p>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="what-about-mutating-data">What about mutating data?<a href="https://trpc.io/blog/introducing-trpc#what-about-mutating-data" class="hash-link" aria-label="Direct link to What about mutating data?" title="Direct link to What about mutating data?">​</a></h2>
<p>Mutations are as simple to do as queries, they're actually the same underneath, but are just exposed differently as syntactic sugar and produce a HTTP POST rather than a GET request.</p>
<p>Here's a little more complicated example using a database, taken from our TodoMVC example at todomvc.trpc.io / <a href="https://github.com/trpc/trpc/tree/main/examples/next-prisma-todomvc" target="_blank" rel="noopener noreferrer">https://github.com/trpc/trpc/tree/main/examples/next-prisma-todomvc</a></p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">todoRouter</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">createRouter</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.mutation</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'add'</span><span style="color: #212121">,</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">  input</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.object</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">    id</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.uuid</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    data</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.object</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      completed</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.boolean</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      text</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">z</span><span style="color: #6F42C1">.string</span><span style="color: #24292EFF">()</span><span style="color: #6F42C1">.min</span><span style="color: #24292EFF">(</span><span style="color: #1976D2">1</span><span style="color: #24292EFF">)</span><span style="color: #6F42C1">.optional</span><span style="color: #24292EFF">()</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  })</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">  </span><span style="color: #D32F2F">async</span><span style="color: #24292EFF"> </span><span style="color: #6F42C1">resolve</span><span style="color: #24292EFF">({ ctx</span><span style="color: #212121">,</span><span style="color: #24292EFF"> input }) {</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> { </span><span style="color: #1976D2">id</span><span style="color: #212121">,</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">data</span><span style="color: #24292EFF"> } </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> input;</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">todo</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">await</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">ctx</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">task</span><span style="color: #6F42C1">.update</span><span style="color: #24292EFF">({</span></div><div class="line"><span style="color: #24292EFF">      where</span><span style="color: #D32F2F">:</span><span style="color: #24292EFF"> { id }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">      data</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">    });</span></div><div class="line"><span style="color: #24292EFF">    </span><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> todo;</span></div><div class="line"><span style="color: #24292EFF">  }</span><span style="color: #212121">,</span></div><div class="line"><span style="color: #24292EFF">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">todoRouter</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">createRouter</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">mutation</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'add'</span><span style="color: #C9D1D9">, {</span></div><div class="line"><span style="color: #C9D1D9">  input: z.</span><span style="color: #D2A8FF">object</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">    id: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">uuid</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    data: z.</span><span style="color: #D2A8FF">object</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      completed: z.</span><span style="color: #D2A8FF">boolean</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">      text: z.</span><span style="color: #D2A8FF">string</span><span style="color: #C9D1D9">().</span><span style="color: #D2A8FF">min</span><span style="color: #C9D1D9">(</span><span style="color: #79C0FF">1</span><span style="color: #C9D1D9">).</span><span style="color: #D2A8FF">optional</span><span style="color: #C9D1D9">(),</span></div><div class="line"><span style="color: #C9D1D9">    }),</span></div><div class="line"><span style="color: #C9D1D9">  }),</span></div><div class="line"><span style="color: #C9D1D9">  </span><span style="color: #FF7B72">async</span><span style="color: #C9D1D9"> </span><span style="color: #D2A8FF">resolve</span><span style="color: #C9D1D9">({ </span><span style="color: #FFA657">ctx</span><span style="color: #C9D1D9">, </span><span style="color: #FFA657">input</span><span style="color: #C9D1D9"> }) {</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> { </span><span style="color: #79C0FF">id</span><span style="color: #C9D1D9">, </span><span style="color: #79C0FF">data</span><span style="color: #C9D1D9"> } </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> input;</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">todo</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">await</span><span style="color: #C9D1D9"> ctx.task.</span><span style="color: #D2A8FF">update</span><span style="color: #C9D1D9">({</span></div><div class="line"><span style="color: #C9D1D9">      where: { id },</span></div><div class="line"><span style="color: #C9D1D9">      data,</span></div><div class="line"><span style="color: #C9D1D9">    });</span></div><div class="line"><span style="color: #C9D1D9">    </span><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> todo;</span></div><div class="line"><span style="color: #C9D1D9">  },</span></div><div class="line"><span style="color: #C9D1D9">});</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<p>And the <strong>React usage</strong> looks like this:</p>
<div><pre class="shiki min-light" style="background-color: #ffffff; color: #24292eff"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">addTask</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">trpc</span><span style="color: #6F42C1">.useMutation</span><span style="color: #24292EFF">(</span><span style="color: #22863A">'todos.add'</span><span style="color: #24292EFF">);</span></div><div class="line"></div><div class="line"><span style="color: #D32F2F">return</span><span style="color: #24292EFF"> (</span></div><div class="line"><span style="color: #24292EFF">  &lt;&gt;</span></div><div class="line"><span style="color: #24292EFF">    &lt;</span><span style="color: #22863A">input</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">placeholder</span><span style="color: #D32F2F">=</span><span style="color: #22863A">"What needs to be done?"</span></div><div class="line"><span style="color: #24292EFF">      </span><span style="color: #6F42C1">onKeyDown</span><span style="color: #D32F2F">=</span><span style="color: #24292EFF">{(e) </span><span style="color: #D32F2F">=&gt;</span><span style="color: #24292EFF"> {</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">const</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">text</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #1976D2">e</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">currentTarget</span><span style="color: #6F42C1">.</span><span style="color: #1976D2">value</span><span style="color: #6F42C1">.trim</span><span style="color: #24292EFF">();</span></div><div class="line"><span style="color: #24292EFF">        </span><span style="color: #D32F2F">if</span><span style="color: #24292EFF"> (</span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.key </span><span style="color: #D32F2F">===</span><span style="color: #24292EFF"> </span><span style="color: #22863A">'Enter'</span><span style="color: #24292EFF"> </span><span style="color: #D32F2F">&amp;&amp;</span><span style="color: #24292EFF"> text) {</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #1976D2">addTask</span><span style="color: #6F42C1">.mutate</span><span style="color: #24292EFF">({ text });</span></div><div class="line"><span style="color: #24292EFF">          </span><span style="color: #1976D2">e</span><span style="color: #24292EFF">.</span><span style="color: #1976D2">currentTarget</span><span style="color: #24292EFF">.value </span><span style="color: #D32F2F">=</span><span style="color: #24292EFF"> </span><span style="color: #22863A">''</span><span style="color: #24292EFF">;</span></div><div class="line"><span style="color: #24292EFF">        }</span></div><div class="line"><span style="color: #24292EFF">      }}</span></div><div class="line"><span style="color: #24292EFF">    /&gt;</span></div><div class="line"><span style="color: #24292EFF">  &lt;/&gt;</span></div><div class="line"><span style="color: #24292EFF">)</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre>
<pre class="shiki github-dark" style="background-color: #0d1117; color: #c9d1d9"><div class="language-id">tsx</div><div class="code-container"><code><div class="line"><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">addTask</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> trpc.</span><span style="color: #D2A8FF">useMutation</span><span style="color: #C9D1D9">(</span><span style="color: #A5D6FF">'todos.add'</span><span style="color: #C9D1D9">);</span></div><div class="line"></div><div class="line"><span style="color: #FF7B72">return</span><span style="color: #C9D1D9"> (</span></div><div class="line"><span style="color: #C9D1D9">  &lt;&gt;</span></div><div class="line"><span style="color: #C9D1D9">    &lt;</span><span style="color: #7EE787">input</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">placeholder</span><span style="color: #FF7B72">=</span><span style="color: #A5D6FF">"What needs to be done?"</span></div><div class="line"><span style="color: #C9D1D9">      </span><span style="color: #79C0FF">onKeyDown</span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9">{(</span><span style="color: #FFA657">e</span><span style="color: #C9D1D9">) </span><span style="color: #FF7B72">=&gt;</span><span style="color: #C9D1D9"> {</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">const</span><span style="color: #C9D1D9"> </span><span style="color: #79C0FF">text</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> e.currentTarget.value.</span><span style="color: #D2A8FF">trim</span><span style="color: #C9D1D9">();</span></div><div class="line"><span style="color: #C9D1D9">        </span><span style="color: #FF7B72">if</span><span style="color: #C9D1D9"> (e.key </span><span style="color: #FF7B72">===</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">'Enter'</span><span style="color: #C9D1D9"> </span><span style="color: #FF7B72">&amp;&amp;</span><span style="color: #C9D1D9"> text) {</span></div><div class="line"><span style="color: #C9D1D9">          addTask.</span><span style="color: #D2A8FF">mutate</span><span style="color: #C9D1D9">({ text });</span></div><div class="line"><span style="color: #C9D1D9">          e.currentTarget.value </span><span style="color: #FF7B72">=</span><span style="color: #C9D1D9"> </span><span style="color: #A5D6FF">''</span><span style="color: #C9D1D9">;</span></div><div class="line"><span style="color: #C9D1D9">        }</span></div><div class="line"><span style="color: #C9D1D9">      }}</span></div><div class="line"><span style="color: #C9D1D9">    /&gt;</span></div><div class="line"><span style="color: #C9D1D9">  &lt;/&gt;</span></div><div class="line"><span style="color: #C9D1D9">)</span></div></code></div><button type="button" aria-label="Copy code to clipboard" class="copy-button" onclick="navigator.clipboard.writeText(this.previousSibling.innerText),this.classList.add(&quot;copied&quot;),this.textContent=&quot;Copied&quot;,setTimeout(()=>{this.classList.remove(&quot;copied&quot;),this.textContent=&quot;Copy&quot;},2e3)">Copy</button></pre></div>
<h2 class="anchor anchorWithStickyNavbar_MYIC" id="end-for-now">End, for now.<a href="https://trpc.io/blog/introducing-trpc#end-for-now" class="hash-link" aria-label="Direct link to End, for now." title="Direct link to End, for now.">​</a></h2>
<p>Anyway, as I said, I just wanted to get the ball rolling. There's a lot more things:</p>
<ul>
<li>Creating context for incoming requests for user-specific data that are dependency injected into the resolvers - <a href="https://trpc.io/docs/v9/context">link</a></li>
<li>Middleware support for routers - <a href="https://trpc.io/docs/v9/middlewares">link</a></li>
<li>Merging routers (you probably don't want all your backend data in one file) - <a href="https://trpc.io/docs/v9/merging-routers">link</a></li>
<li>Simplest server-side rendering you've ever seen in React-land using our <code>@trpc/next</code> adapter - <a href="https://trpc.io/docs/v9/">link</a></li>
<li>Type-safe error formatting - <a href="https://trpc.io/docs/v9/error-formatting">link</a></li>
<li>Data transformers (use Date/Map/Set objects across the wire) - <a href="https://trpc.io/docs/v9/data-transformers">link</a></li>
<li>Helpers for React Query</li>
</ul>
<p>If you want to get started there's a few examples in the <a href="https://trpc.io/docs/v9/nextjs">Getting Started for Next.js</a>.</p>
<p><a href="https://twitter.com/alexdotjs" target="_blank" rel="noopener noreferrer">Follow me on Twitter for updates!</a></p>]]></content:encoded>
        </item>
    </channel>
</rss>