Table Of Contents
Introduction
When working with a modern UI library, we often need to fetch data from an external source, a REST or GraphQL API. Synchronizing the client with a server state comes with some challenges, including type definitions and safety. All the external data sources define schemas, but they are not immediately consumable from our UI without redefining them, leveraging types generations libraries, or importing monorepo packages. The lack of immediately available types and the introduction of more dedicated tools create friction and slow developers’ productivity and velocity. Suppose your organization is also split into a front-end and back-end team. The communication overhead adds a layer of potential conflicts because the two parts must agree on the schema before starting the development of features.
tRPC provides all the tools to create full-stack end-to-end type-safe applications. It is a library that combines the power of TypeScript and react-query to register all the server endpoints and make them instantly available with types to the client. It increases team productivity because backend endpoints are just functions, and the developers can effectively work on the entire stack. Any team can significantly benefit from this tool because code generation, types package, and communication overhead are phased out.
tRPC also has some downsides. A single developer maintains it. Files cannot be uploaded without any 3rd parties services like Amazon S3. WebSockets have limited support, and data is limited to JSON.
Pros
- Full-stack end-to-end type-safe application development.
- Better team productivity and velocity.
- Minimal communication overhead.
- If you trust the type system and there is no complex business logic, unit tests may not be required.
Cons
- Solo maintainer.
- TypeScript could slow down your IDE.
- Data is limited to JSON.
- Limited support for WebSocket.
tRPC Tutorial
tRPC Prerequisites
The tutorial assumes you are familiar with the following libraries:
- React,
- Next.js,
- Prisma or other ORMs,
- react-query.
tRPC project bootstrap
Let’s initialize a new tRPC project with the create-t3-app
CLI:
npx create-t3-app@latest
You will be prompted to answer some questions:
- Name your application.
- Select TypeScript.
- Select Prisma and tRPC.
- Initialize a git repository.
- Run
npm install
.
Once the project is bootstrapped, navigate to prisma/schema.prisma
and update it with a new Blog
model.
model Blog {
id String @id @default(cuid())
title String
content String
createdAt DateTime @default(now())
}
From your terminal, run npx prisma db push
to apply the model to a local SQLite database.
tRPC Backend development
We are ready to work on the backend of our application. Navigate to src/server/router
and create a new file named blog.ts.
Please copy the following code to create two endpoints. One handles the creation of a blog post, and the other lists the inserted articles.
import { createRouter } from "./context";
// `zod` is a schema declaration and validation library. We use it to define the shape of request arguments.
import { z } from "zod";
// `createRouter` exposes functions to create new API endpoints.
export const blogRouter = createRouter()
// A `mutation` defines an operation that modifies our data.
// It behaves like`POST`, `PUT`, `DELETE` operations.
// The input property accepts an optional `zod` schema to validate the request body.
.mutation("create", {
input: z.object({
title: z.string(),
content: z.string(),
}),
// The API handler is a simple `resolve` function that contains all the business logic to handle the request.
resolve({ input, ctx }) {
return ctx.prisma.blog.create({
data: {
title: input.title,
content: input.content,
},
});
},
})
// A `query` defines an operation that reads data from our backend.
// It behaves like a GET request.
.query("all", {
resolve({ ctx }) {
return ctx.prisma.blog.findMany();
},
});
Once we have defined the routes, we can register them in src/server/router/index.ts.
// src/server/router/index.ts
import { createRouter } from "./context";
import superjson from "superjson";
import { blogRouter } from "./blog";
export const appRouter = createRouter()
.transformer(superjson)
// Registering the blogRouter
.merge("blog.", blogRouter);
// export type definition of the API
export type AppRouter = typeof appRouter;
UI development
Navigate to src/pages/index.tsx
and paste the following code. The tRPC utilities that wrap react-query enable us to access the defined type-safe backend procedures.
import type { NextPage } from "next";
import { useState } from "react";
import BlogPost from "../components/BlogPost";
import Layout from "../components/Layout";
// import trpc utils that wrap react-query
import { trpc } from "../utils/trpc";
const Home: NextPage = () => {
// Hovering on data shows the types defined in our backend
const { isLoading, data } = trpc.useQuery(["blog.all"]);
return (
<Layout>
<h1>My personal blog</h1>
{isLoading ? (
<div>Loading posts</div>
) : (
data?.map((blog) => <BlogPost blog={blog} key={blog.id} />)
)}
<CreatePostSection />
</Layout>
);
};
export default Home;
const CreatePostSection = () => {
const [title, setTitle] = useState("");
const [content, setContent] = useState("");
const ctx = trpc.useContext();
const createMutation = trpc.useMutation("blog.create");
return (
<div>
{createMutation.isLoading}
<h2>Create a new blog post</h2>
<div>
<input
name="title"
placeholder="Your title..."
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div>
<textarea
rows={10}
cols={50}
value={content}
placeholder="Start typing..."
onChange={(e) => setContent(e.target.value)}
/>
</div>
<div>
<button
onClick={() =>
// mutate exposes the types of arguments as defined in our router
createMutation.mutate(
{ title, content },
{
onSuccess() {
ctx.invalidateQueries("blog.all");
setContent("");
setTitle("");
},
}
)
}
disabled={createMutation.isLoading}
>
Create new post
</button>
</div>
</div>
);
};
Run npm run dev
to check the platform. You can further practice with tRPC by expanding the application with new features. Some examples are finding by id, deleting, or updating a single post. A complete solution can be found here https://github.com/andrew-hu368/t3-blog.
Top comments (0)