DEV Community

Cover image for tRPC 101: Concepts and some code experiments with create T3 app
Yuko
Yuko

Posted on

tRPC 101: Concepts and some code experiments with create T3 app

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.


eConcepts | tRPC

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)
}
Enter fullscreen mode Exit fullscreen mode

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;
        }),
    });


Enter fullscreen mode Exit fullscreen mode


    // 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;


Enter fullscreen mode Exit fullscreen mode


    // 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,
    });


Enter fullscreen mode Exit fullscreen mode

💡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);


Enter fullscreen mode Exit fullscreen mode

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
        </>
      );
    }


Enter fullscreen mode Exit fullscreen mode

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;


Enter fullscreen mode Exit fullscreen mode

That’s it! Thank your reading :)
Code is available here

Top comments (0)