Skip to main content

Β· 4 min read
Alex / KATT 🐱

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.

Since our last major version release in August 2021, the tRPC community has seen substantial growth:

Today, we're launching tRPC v10. 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.

For new projects, you can get up and running with an example application to learn about tRPC v10. For projects that were already enjoying tRPC v9, visit the v10 migration guide.

Overview of changes​

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.

Improved developer experience​

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.

With v10, you can:

  • Use "Go to Definiton" to jump straight from your frontend consumer to your backend procedure
  • Use "Rename Symbol" to give a new name to an input argument or procedure across your whole application
  • Infer types more easily for when you'd like to use your tRPC types in your application manually

Powerful backend framework​

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:

Massively improved TypeScript performance​

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.

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.

Incremental migration​

We've also put in a lot of work to make the migration experience as straightforward as possible, including an interop() method that allows (almost) full backward compatibility with v9 routers. Visit the migration guide for more information.

Sachin from the core team has also made a codemod that can do much of the heavy lifting of the migration for you.

A growing ecosystem​

A rich set of sub-libraries is continuing to form around tRPC. Here are a few examples:

For more plugins, examples, and adapters, visit the Awesome tRPC collection.

Thank you!​

The core team and I want you to know: we're just getting started. We're already busy experimenting with React Server Components and Next.js 13.

I also want to give a huuuge shoutout to Sachin, Julius, James, Ahmed, Chris, Theo, Anthony, and all the contributors who helped make this release possible.

Thanks for using and supporting tRPC.


Β· 5 min read
Alex / KATT 🐱

I'm Alex, or "KATT" on GitHub, and I want to tell you about a library called tRPC. I've not published any articles about, so I'm just writing this intro to get the ball rolling (but we have already somehow reached >530 🌟 on GitHub). Expect articles & video intros to come! If you want to stay up-to-date or want to ask questions, you can follow me on Twitter at @alexdotjs.

In short - 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.

This is how it can look like when doing a tRPC endpoint & client call: Alt Text

I have made a library for React (@trpc/react) that sits on top of the great react-query, but the client library (@trpc/client) works without React (if you want to build a specific Svelte/Vue/Angular/[..] lib, please reach out!)

There's no code generation involved & you can pretty easily add it to your existing Next.js/CRA/Express project.

Example​

Here's an example of a tRPC procedure (aka endpoint) called helloΒ that takes a string argument.

tsx
const appRouter = trpc.router().query('hello', {
input: z.string().optional(),
resolve: ({ input }) => {
return {
text: `hello ${input ?? 'world'}`,
};
},
});
export type AppRouter = typeof appRouter;
tsx
const appRouter = trpc.router().query('hello', {
input: z.string().optional(),
resolve: ({ input }) => {
return {
text: `hello ${input ?? 'world'}`,
};
},
});
export type AppRouter = typeof appRouter;

And here's a type safe client using said data:

tsx
import type { AppRouter } from './server';
async function main() {
const client = createTRPCClient<AppRouter>({
url: `http://localhost:2022`,
});
const result = await client.query('hello', '@alexdotjs');
console.log(result); // --> { text: "hello @alexdotjs" }
}
main();
tsx
import type { AppRouter } from './server';
async function main() {
const client = createTRPCClient<AppRouter>({
url: `http://localhost:2022`,
});
const result = await client.query('hello', '@alexdotjs');
console.log(result); // --> { text: "hello @alexdotjs" }
}
main();

That's all you need to get type safety! The result 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 have to pass the input data through a validator (& tRPC works out-of-the-box with zod/yup/custom validators).

Here's a CodeSandbox link where you can play with the example above: https://githubbox.com/trpc/trpc/tree/main/examples/standalone-server (have a look at the terminal output rather than the preview!)

Wat? I'm importing code from my backend to my client? - No, you're actually not

Even though it might look like it, no code is shared from the server to the client; TypeScript's import type "[..] 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 - see TypeScript docs.

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

But we're only getting started!​

I mentioned before that there's a React-library, the way to use the data above in React you do:

tsx
const { data } = trpc.useQuery(['hello', '@alexdotjs']);
tsx
const { data } = trpc.useQuery(['hello', '@alexdotjs']);

.. and you'll get type safe data on the client.

You can add tRPC today with your existing brownfield project (got adapters for Express/Next.js) & 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.

What about mutating data?​

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.

Here's a little more complicated example using a database, taken from our TodoMVC example at todomvc.trpc.io / https://github.com/trpc/trpc/tree/main/examples/next-prisma-todomvc

tsx
const todoRouter = createRouter().mutation('add', {
input: z.object({
id: z.string().uuid(),
data: z.object({
completed: z.boolean().optional(),
text: z.string().min(1).optional(),
}),
}),
async resolve({ ctx, input }) {
const { id, data } = input;
const todo = await ctx.task.update({
where: { id },
data,
});
return todo;
},
});
tsx
const todoRouter = createRouter().mutation('add', {
input: z.object({
id: z.string().uuid(),
data: z.object({
completed: z.boolean().optional(),
text: z.string().min(1).optional(),
}),
}),
async resolve({ ctx, input }) {
const { id, data } = input;
const todo = await ctx.task.update({
where: { id },
data,
});
return todo;
},
});

And the React usage looks like this:

tsx
const addTask = trpc.useMutation('todos.add');
return (
<>
<input
placeholder="What needs to be done?"
onKeyDown={(e) => {
const text = e.currentTarget.value.trim();
if (e.key === 'Enter' && text) {
addTask.mutate({ text });
e.currentTarget.value = '';
}
}}
/>
</>
)
tsx
const addTask = trpc.useMutation('todos.add');
return (
<>
<input
placeholder="What needs to be done?"
onKeyDown={(e) => {
const text = e.currentTarget.value.trim();
if (e.key === 'Enter' && text) {
addTask.mutate({ text });
e.currentTarget.value = '';
}
}}
/>
</>
)

End, for now.​

Anyway, as I said, I just wanted to get the ball rolling. There's a lot more things:

  • Creating context for incoming requests for user-specific data that are dependency injected into the resolvers - link
  • Middleware support for routers - link
  • Merging routers (you probably don't want all your backend data in one file) - link
  • Simplest server-side rendering you've ever seen in React-land using our @trpc/next adapter - link
  • Type-safe error formatting - link
  • Data transformers (use Date/Map/Set objects across the wire) - link
  • Helpers for React Query

If you want to get started there's a few examples in the Getting Started for Next.js.

Follow me on Twitter for updates!