tRPC v10 Fast Hands-on
Last year I wrote a tRPC hands-on. And I’ve glad to read it from many developers.
However, tRPC is now v10 released, and there are many breaking changes, so the above hands-on can’t use.
Therefore, in this post, I’ll show you tRPC@10 hands-on. Please give it a try.
StackBlitz ready
If you click the bellow button, you can proceed with this hands-on on StackBlitz.(Of course, you can do it locally too.)
You should see a page like this when you click the button. This is the StackBlitz loading screen.
After about 20 seconds, you’ll see the page change like this. This is the starter template page.
On this page, you will display some posts retrieved by tRPC and also allow you to create posts by building a typesafe API with tRPC.
Defining a backend router
To start, this API will only contain two endpoints with these TypeScript signatures:
posts() => { id: string; title: string; }
postCreate(data: { title: string }) => { id: string; title: string; }
Create a router instance.
First, let’s define an empty router in our server codebase. Create sever/routers/_app.ts
as follows:
import { initTRPC } from "@trpc/server";
const t = initTRPC.create();
export const appRouter = t.router({});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
Add a query procedure
Use the t.procedure.query()
to add a query procedure/endpoint to the router. The two methods on this procedure are:
input
(optional): When provided, this should be a function that validates and casts the input of this procedure. It should return a strongly typed value when the input is valid or throw an error if the input is invalid. We recommend using a TypeScript validation library like Zod, Superstruct or Yup for input validation.query
: This is the implementation of the procedure (a “resolver”). It’s a function with a singlereq
argument to represent the incoming request. The validated (and strongly typed!) input is passed intoreq.input
.
The following creates a query procedure called posts
that returns posts object: (Highlight the changed line)
import * as trpc from "@trpc/server";
import { z } from 'zod';
const t = initTRPC.create();
interface Post {
id: string;
title: string;
}
const postList: Post[] = [
{
id: "1",
title: "Hello tRPC!!",
},
{
id: "2",
title: "How are you?",
}
];
export const appRouter = t.router({
posts: t.procedure
.input(
z
.object({
filter: z.string(),
})
.nullish()
)
.query(({ input }) => {
if (input?.filter == null || input.filter == '') {
return postList;
}
return postList.filter((post) => post.title.includes(input.filter));
}),
});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
Add a mutation procedure
Similar to GraphQL, tRPC makes a distinction between query and mutation procedures.
The way a procedure works on the server doesn’t change much between a query and a mutation. The method name is different, and the way that the client will use this procedure changes - but everything else is the same!
Let’s add a postCreate mutation by adding it as a new property on our router object:
import * as trpc from "@trpc/server";
import { z } from 'zod';
const t = initTRPC.create();
interface Post {
id: string;
title: string;
}
const postList: Post[] = [
{
id: "1",
title: "Hello tRPC!!",
},
{
id: "2",
title: "How are you?",
}
];
export const appRouter = t.router({
posts: t.procedure
.input(
z
.object({
filter: z.string(),
})
.nullish()
)
.query(({ input }) => {
if (input?.filter == null || input.filter == '') {
return postList;
}
return postList.filter((post) => post.title.includes(input.filter));
}),
postCreate: t.procedure
.input(z.object({ title: z.string() }))
.mutation((req) => {
const id = `${Math.random()}`;
const post: Post = {
id,
title: req.input.title,
};
postList.push(post);
return post;
}),
});
// Export type router type signature,
// NOT the router itself.
export type AppRouter = typeof appRouter;
Export API handler
Next, expose your router as an API of Next.js.
Create pages/api/trpc/[trpc].ts
and do the following:
import * as trpcNext from '@trpc/server/adapters/next';
import { appRouter } from '../../../server/routers/_app';
// export API handler
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: () => ({}),
});
Now, you finished to create router 🎉 Next, you will create a client with Next.js.
Create tRPC utility for frontend and interact with a backend router
Create a tRPC utility for Next.js using your API’s type signature.
Let’s create a utils/trpc.ts
file with the following contents:
import { httpBatchLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { AppRouter } from '../server/routers/_app'
export const trpc = createTRPCNext<AppRouter>({
config({ ctx }) {
return {
links: [
httpBatchLink({
url: '/api/trpc/',
}),
],
};
},
});
Configure _app.tsx
import "../styles/globals.css";
import { AppType } from "next/dist/shared/lib/utils";
import { trpc } from '../utils/trpc';
const MyApp: AppType = ({ Component, pageProps }) => {
return <Component {...pageProps} />;
};
export default trpc.withTRPC(MyApp);
You’re ready! Let’s connect the server created now.
Use query
Open src/index.tsx
then, add import trpc hook
.
import type { NextPage } from "next";
import { FormEvent, useState } from "react";
import styles from "../styles/Home.module.css";
import { trpc } from "../utils/trpc";
And, use tRPC query hook like this.
I strongly recommend typing it instead of copy and paste it.
const [title, setTitle] = useState("");
const [filter, setFilter] = useState("");
const [error, setError] = useState("");
const query = trpc.posts.useQuery(); // 👈 Let's type it!
When you type const query = trpc.
, I think you see posts
as a candidate, is it working?
This is possible because tRPC use type information of router that you just created as the scheme for client. You don’t need to generate some file like GraphQL and OpenAPI!
Next, display query result like this.
return (
<section className={styles.container}>
<main className={styles.main}>
<header className={styles.title}>
<h1>tRPC fast hands-on 🚀</h1>
</header>
<section className={styles.controller}>
{/* form component */}
</section>
<section className={styles.grid}>
{query.data?.map((data, i) => (
<article key={`article-${i}`} className={styles.card}>
<p>{data.title}</p>
</article>
))}
</section>
</main>
</section>
);
query.data
is also type safe, so you will type data.
inside callback function in map then, appears candidates which id
and title
.
You see the following display on your local server, right?
Use query variable
You can filter posts using filter
parameter of posts
query so let’s set input text to query variables.
Set filter state to useQuery args:
const [filter, setFilter] = useState("");
const [error, setError] = useState("");
const query = trpc.posts.useQuery({ filter })
note
If you type useQuery({ f
, editor suggests filter
field.
This is another power of tRPC. You can use the input scheme declared on the server as the interface of the query variable on the client.
Input scheme is declared on following:
export const appRouter = t.router({
posts: t.procedure
.input(
z
.object({
filter: z.string(),
})
.nullish()
)
.query(({ input }) => {
if (input?.filter == null || input.filter == '') {
return postList;
}
return postList.filter((post) => post.title.includes(input.filter));
}),
Use mutation
In the last of this hands-on, let you create a mutation with tRPC.
Since the form UI is already implemented, you can use the title
variable as an input variable in the postCreate mutation.
First, add mutation hook:
const query = trpc.posts.useQuery({ filter });
const mutation = trpc.postCreate.useMutation();
Next, mutate on submitting the form.
async function submitNewPost(e: FormEvent) {
e.preventDefault();
mutation.mutate({ title })
}
Then, you can create a post with tRPC!
Conclusion
I don’t think tRPC will solve all problems like the Swiss Army Knife or Silver Bullet, but I do think it will significantly increase the speed of launching a product.
Especially I consider it has a high affinity for developer who want to build type-safe applications but feel GraphQL and OpenAPI are overreaching.
Some of you who have read this far may have the following questions:
-
“I know the basics, but can I implement a real world application or use case?”
-
“Can I implement authentication and error handling?”
-
“Can I implement localization?”
The answer is yes. I deploy application with tRPC to Vercel and get paid by my clients. This application uses firebase authentication to authenticate user and UI is Japanese.
Once again, I will explain how to authenticate and localize and more. If you are curios it, please follow my twitter account, I’ll let you know.