tRPC 101: Concepts and some code experiments with create T3 app
Published: 2023-07-12 05:25:33


## 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](https://create.t3.gg/).

## Concept

RPC stands for “Remote Procedure Call,” and tRPC enables calling functions on a server computer from a client computer ([It’s just functions](https://trpc.io/docs/concepts#its-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](https://trpc.io/docs/concepts#dont-think-about-httprest-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 ↗**](https://trpc.io/docs/server/procedures): 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**](https://trpc.io/docs/subscriptions): A procedure that creates a persistent connection and listens to changes.
[**Router ↗**](https://trpc.io/docs/server/routers): A collection of procedures (and/or other routers) under a shared namespace.
[**Context ↗**](https://trpc.io/docs/server/context): Stuff that every procedure can access. Commonly used for things like session state and database connections.
[**Middleware ↗**](https://trpc.io/docs/server/middlewares): A function that can run code before and after a procedure. Can modify context.
[**Validation ↗**](https://trpc.io/docs/server/procedures#input-validation):”Does this input data contain the right stuff?”

This is my image of tRPC ecosystem.

![](https://cdn-images-1.medium.com/max/3600/1*QTg9ONC4kQdazma7igFQoQ.png)
[**eConcepts | tRPC**](https://trpc.io/docs/concepts#vocabulary)

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

![](https://cdn-images-1.medium.com/max/3604/1*WaS6IaGnCzZht2TEDh56dQ.png)

**Schema**
These are schema for tweet and users ([How to setup Prisma?](https://medium.com/@lada496/how-to-setup-prisma-with-your-local-postgresql-a82407ff823e), [What’s PostgreSQL?](https://medium.com/@lada496/postgresql-101-from-whats-database-to-basic-sql-queries-1aea5caed904)).

    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](https://authjs.dev/reference/adapters).

### Setup tRPC

I will explain the process along with [the official documents recipe](https://trpc.io/docs/quickstart).

[1️⃣ Install deps](https://trpc.io/docs/client/nextjs/setup#1-install-deps) (✅ done by T3 app)
[2️⃣ Enable strict mode](https://trpc.io/docs/client/nextjs/setup#2-enable-strict-mode) (✅ done by T3 app)
[3️⃣ Create a tRPC router](https://trpc.io/docs/client/nextjs/setup#3-create-a-trpc-router)
```typescript
    // 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;
        }),
    });
```
```typescript
    // 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;
```
```typescript
    // 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.
```typescript
    // 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](https://trpc.io/docs/client/nextjs/setup#4-create-trpc-hooks) (✅ done by T3 app: src/utils/api.ts)
[5️⃣ Configure _app.tsx](https://trpc.io/docs/client/nextjs/setup#5-configure-_apptsx) (✅ done by T3 app)
[6️⃣ Make an API request](https://trpc.io/docs/client/nextjs/setup#3-create-a-trpc-router)
```typescript
    // 
    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](https://create.t3.gg/en/usage/trpc#optimistic-updates).
```typescript
    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](https://github.com/Lada496/my-t3-twitter-lite)