tRPC 101: Concepts and some code experiments with create T3 app
In this article, I will share what I’ve learned about tRPC with an example app built with create T3 app.
Concept
RPC stands for “Remote Procedure Call,” and tRPC enables calling functions on a server computer from a client computer (It’s just functions). Instead of manually making HTTP requests to specific URLs and handling responses, tRPC provides a type-safe functions by abstracting away the underlying HTTP communication (Don’t think about HTTP/REST implementation details). With tRPC, developers can focus on writing code and interacting with type-safe functions rather than managing network communication.
Vocabulary
Here are the official documents’ explanation of main terminologies.
Procedure ↗: API endpoint — can be a query, mutation, or subscription.
Query: A procedure that gets some data.
Mutation: A procedure that creates, updates, or deletes some data.
Subscription: A procedure that creates a persistent connection and listens to changes.
Router ↗: A collection of procedures (and/or other routers) under a shared namespace.
Context ↗: Stuff that every procedure can access. Commonly used for things like session state and database connections.
Middleware ↗: A function that can run code before and after a procedure. Can modify context.
Validation ↗:”Does this input data contain the right stuff?”
This is my image of tRPC ecosystem.
Try tRPC
Overview
This is a high level overview of my app: A very lite version of Twitter where you can view all tweets and post/delete your tweets.
Schema
These are schema for tweet and users (How to setup Prisma?, What’s PostgreSQL?).
model User {
id String @id @default(uuid())
name String?
email String? @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
tweets Tweet[]
}
model Tweet {
id Int @id @default(autoincrement())
content String
createdAt DateTime @default(now())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}
For more information for nextAuth related scheme: Models.
Setup tRPC
I will explain the process along with the official documents recipe.
1️⃣ Install deps (✅ done by T3 app)
2️⃣ Enable strict mode (✅ done by T3 app)
3️⃣ Create a tRPC router
// src/server/api/routers/tweet.ts
// Create a router
import { z } from "zod";
import {
createTRPCRouter,
publicProcedure,
protectedProcedure,
} from "~/server/api/trpc";
export const tweetRouter = createTRPCRouter({
all: publicProcedure.query(({ ctx }) => {
return ctx.prisma.tweet.findMany({
select: {
id: true,
content: true,
createdAt: true,
userId: true,
user: { select: { name: true } },
},
orderBy: {
createdAt: "desc",
},
});
}),
oneUser: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ ctx, input }) => {
const { id } = input;
return ctx.prisma.tweet.findMany({
where: { userId: id },
select: {
id: true,
content: true,
createdAt: true,
userId: true,
user: { select: { name: true } },
},
orderBy: {
createdAt: "desc",
},
});
}),
add: protectedProcedure
.input(z.object({ text: z.string() }))
.mutation(({ ctx, input }) => {
const userId = ctx.session.user.id;
const tweet = ctx.prisma.tweet.create({
data: {
content: input.text,
userId,
},
});
return tweet;
}),
delete: protectedProcedure
.input(z.object({ id: z.number() }))
.mutation(({ ctx, input }) => {
const deletedTweet = ctx.prisma.tweet.delete({
where: { id: input.id },
});
return deletedTweet;
}),
});
// src/server/api/root.ts
// add the router
import { tweetRouter } from "./routers/tweet";
import { createTRPCRouter } from "~/server/api/trpc";
/**
* This is the primary router for your server.
*
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
tweet: tweetRouter,
});
// export type definition of API
export type AppRouter = typeof appRouter;
// src/pages/api/trpc/[trpc].ts
// serve tRPC router (✅ done by T3 app)
// for more advanced usage -> https://trpc.io/docs/server/adapters/nextjs#handling-cors-and-other-advanced-usage
import { createNextApiHandler } from "@trpc/server/adapters/next";
import { env } from "~/env.mjs";
import { appRouter } from "~/server/api/root";
import { createTRPCContext } from "~/server/api/trpc";
// export API handler
export default createNextApiHandler({
router: appRouter,
createContext: createTRPCContext,
onError:
env.NODE_ENV === "development"
? ({ path, error }) => {
console.error(
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
);
}
: undefined,
});
💡T3 app provides us createTRPCContext so that we can access prisma with ctx during procedures. T3 also set createTRPCRouter,publicProcedure, and protectedProcedure with the context.
// src/server/api/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { type CreateNextContextOptions } from "@trpc/server/adapters/next";
import { type Session } from "next-auth";
import superjson from "superjson";
import { ZodError } from "zod";
import { getServerAuthSession } from "~/server/auth";
import { prisma } from "~/server/db";
type CreateContextOptions = {
session: Session | null;
};
const createInnerTRPCContext = (opts: CreateContextOptions) => {
return {
session: opts.session,
prisma,
};
};
// This code allows procedures to access session and prisma
export const createTRPCContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
// Get the session from the server using the getServerSession wrapper function
const session = await getServerAuthSession({ req, res });
return createInnerTRPCContext({
session,
});
};
// Initialization with connecting context
const t = initTRPC.context<typeof createTRPCContext>().create({
transformer: superjson,
errorFormatter({ shape, error }) {
return {
...shape,
data: {
...shape.data,
zodError:
error.cause instanceof ZodError ? error.cause.flatten() : null,
},
};
},
});
// to create routers
export const createTRPCRouter = t.router;
// Public (unauthenticated) procedure
const publicProcedure = t.procedure;
/** Reusable middleware that enforces users are logged in before running the procedure. */
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
if (!ctx.session || !ctx.session.user) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return next({
ctx: {
// infers the `session` as non-nullable
session: { ...ctx.session, user: ctx.session.user },
},
});
});
// Protected (authenticated) procedure
export const protectedProcedure = t.procedure.use(enforceUserIsAuthed);
4️⃣ Create tRPC hooks (✅ done by T3 app: src/utils/api.ts)
5️⃣ Configure _app.tsx (✅ done by T3 app)
6️⃣ Make an API request
//
import Head from "next/head";
import { api } from "~/utils/api";
import TweetItem from "~/components/TweetItem";
export default function Home() {
const { data: tweets } = api.tweet.all.useQuery();
return (
<>
...contents
</>
);
}
For delete and add mutation, I tried Optimistic updates.
import React from "react";
import { useSession } from "next-auth/react";
import FeatherIcon from "feather-icons-react";
import { type RouterOutputs } from "~/utils/api";
import { api } from "~/utils/api";
type ArryItems<T> = T extends (infer Item)[] ? Item : T;
type TweetItem = ArryItems<RouterOutputs["tweet"]["oneUser"]>;
function TweetItem({ tweet }: { tweet: TweetItem }) {
const { data: sessionData } = useSession();
const utils = api.useContext();
const mutation = api.tweet.delete.useMutation({
async onMutate(id) {
await utils.tweet.oneUser.cancel();
const prevData = utils.tweet.oneUser.getData({ id: tweet.userId });
utils.tweet.oneUser.setData({ id: tweet.userId }, (old) =>
old?.filter((tweet) => tweet.id !== id)
);
return { prevData };
},
onError(err, id, ctx) {
// If the mutation fails, use the context-value from onMutate
utils.tweet.oneUser.setData({ id: tweet.userId }, ctx?.prevData);
},
async onSettled() {
// Sync with server once mutation has settled
await utils.tweet.oneUser.invalidate();
},
});
const handleDelete = () => {
if (confirm("Are you sure to delete the tweet?")) {
mutation.mutate({ id: tweet.id });
}
};
return (
...content
);
}
export default TweetItem;
That’s it! Thank your reading :)
Code is available here
Top comments (0)