Test Category

Test Blog Post

Starter template for writing out a blog post using MDX/JSX and Next.js.

No Name Exists

Abdullah Muhammad

Published on May 17, 20265 min read 1 views

Share:
Article Cover Image

Introduction

One of the most underrated libraries out there is tRPC. It works well with TypeScript development and streamlines the work process.

Think of it as an intelligent way to help your front-end and back-and communicate with each other in a unique, type-safe way.

We often have to create custom data types for working with data fetch requests (as you have seen numerous times) with Next.js and the MERN stack.

tRPC offers a unique way of being able to access custom types defined within your API routes.

We do away with REST API development and instead work with a server that creates and handles requests through procedures.

We utilize the Zod schema library (covered here) to validate input data.

There are two main types of procedures we are concerned with:

  • Queries
  • Mutations

Similar to ReactQuery where users make calls to the back-end, tRPC allows one to use a server that defines a tRPC server and further defines procedures that handle specific requests.

As you saw with ReactQuery, the useQuery hook allows one to gather data (GET requests) while useMutation allows one to modify and update data (POST, PUT, DELETE requests).

You get type safety at each point of communication between the front-end and back-end without the need of having to check and define types to ensure adherence.

There is boilerplate code you will need to get familiar with such as setting up a tRPC server on the server-side and a client that communicates with this server from the front-end.

We will use tRPC to enable CRUD operations using procedures and the Drizzle ORM library (we covered that here) to work with User objects inside a Supabase database.

Folder Structure

There is quite a bit of boilerplate code to get to so let us not waste time.

The following picture details the layout of working with the tRPC library for development:

No Image Found
Folder structure for setting up tRPC for your project

Initialize the tRPC Server

We first have to initialize a tRPC server which handles all of the different routes. These routes are referred to as procedures.

The following code block details how we setup that tRPC server:

GitHub GistTypeScript
import { initTRPC } from '@trpc/server';
import { ZodError } from 'zod';

// Initialize the TRPC server
const t = initTRPC.create({
  errorFormatter({ shape, error }) {
    return {
      ...shape,
      data: {
        ...shape.data,
        zodError:
          error.cause instanceof ZodError ? error.cause.flatten() : null,
      },
    };
  },
});

export const createTRPCRouter = t.router;
export const publicProcedure = t.procedure;
Initializing the tRPC server for creating different procedures

Connect Drizzle to Supabase

We now use Drizzle ORM to interact with Supabase and setup a User schema using the Drizzle library.

GitHub GistTypeScript
import { pgTable, serial, timestamp, varchar } from 'drizzle-orm/pg-core';

// User Table Schema setup complete
export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: varchar('name', { length: 255 }).notNull(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  createdAt: timestamp('created_at').defaultNow().notNull(),
  updatedAt: timestamp('updated_at').defaultNow().notNull()
});

export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
Setting up the Drizzle User schema for working with a user table

We will use this schema to later process database requests. It is helpful to re-visit the Drizzle ORM docs if something is confusing to you.


Create tRPC Routers

In tRPC, we have routers and procedures. The functions that handle the requests are referred to as procedures.

The following defines a public procedure for working with users:

GitHub GistTypeScript
import { z } from 'zod';
import { eq } from 'drizzle-orm';
import { createTRPCRouter, publicProcedure } from '../trpc';
import { db } from '../../../db';
import { users } from '../../../db/schema';

// User router for working with the different CRUD operations
export const usersRouter = createTRPCRouter({
  // Create User
  create: publicProcedure
    .input(
      z.object({
        name: z.string().min(1),
        email: z.string().email()
      })
    )
    .mutation(async ({ input }) => {
      const [user] = await db
        .insert(users)
        .values({
          name: input.name,
          email: input.email
        })
        .returning();
      
      return user;
    }),

  // Get All Users
  getAll: publicProcedure.query(async () => {
    return await db.select().from(users);
  }),

  // Get User by ID
  getById: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(async ({ input }) => {
      const [user] = await db
        .select()
        .from(users)
        .where(eq(users.id, input.id));
      
      return user || null;
    }),

  // Update user
  update: publicProcedure
    .input(
      z.object({
        id: z.number(),
        name: z.string().min(1).optional(),
        email: z.string().email().optional(),
      })
    )
    .mutation(async ({ input }) => {
      const updateData: any = { updatedAt: new Date() };
      
      if (input.name !== undefined) updateData.name = input.name;
      if (input.email !== undefined) updateData.email = input.email;

      const [updatedUser] = await db
        .update(users)
        .set(updateData)
        .where(eq(users.id, input.id))
        .returning();
      
      return updatedUser;
    }),

  // Delete user
  delete: publicProcedure
    .input(z.object({ id: z.number() }))
    .mutation(async ({ input }) => {
      const [deletedUser] = await db
        .delete(users)
        .where(eq(users.id, input.id))
        .returning();
      
      return deletedUser;
    })
});
Setting up a user procedure for working with different methods

Combine Routers

Combine all routers (you may have multiple, e.g., user, post, auth) into a single root router. This helps keep things in order:

GitHub GistTypeScript
import { createTRPCRouter } from './trpc';
import { usersRouter } from './routers/users';

// Router for working with the User object inside the Supabase database
export const appRouter = createTRPCRouter({
  users: usersRouter
});

export type AppRouter = typeof appRouter;
Combining different routes into one main router

Expose the API via Next.js Route Handler

Once all this has been setup, we can proceed to exposing the tRPC router using the Next.js App Router route handler.

Next, we expose the tRPC router using the Next.js App Router route handler:

GitHub GistTypeScript
import { fetchRequestHandler } from '@trpc/server/adapters/fetch';
import { appRouter } from '../../../../server/api/root';

// Handler for the TRPC API route
const handler = (req: Request) =>
  fetchRequestHandler({
    endpoint: '/api/trpc',
    req,
    router: appRouter,
    createContext: () => ({})
  });

export { handler as GET, handler as POST };
Handler function for working with the tRPC API route

Create the tRPC Client

To consume the API from React components, initialize a client:

GitHub GistTypeScript
'use client';

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpBatchLink } from '@trpc/client';
import { createTRPCReact } from '@trpc/react-query';
import { useState } from 'react';
import type { AppRouter } from '../../server/api/root';

const trpc = createTRPCReact<AppRouter>();

// TRPC provider setup client-side
export function TRPCProvider({ children }: { children: React.ReactNode }) {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: '/api/trpc'
        })
      ]
    })
  );

  return (
    <trpc.Provider client={trpcClient} queryClient={queryClient}>
      <QueryClientProvider client={queryClient}>
        {children}
      </QueryClientProvider>
    </trpc.Provider>
  );
}

export { trpc };
tRPC getting setup for usage client-side

Use tRPC in Your Frontend

Now, with the tRPC client setup, we can readily use it in the front-end React components. The following code snippet illustrates this in great detail:

GitHub GistTSX
'use client';

import { useState } from 'react';
import { trpc } from './components/providers';

export default function Home() {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [editingId, setEditingId] = useState<number | null>(null);

  const users = trpc.users.getAll.useQuery();
  const createUser = trpc.users.create.useMutation({
    onSuccess: () => {
      users.refetch();
      setName('');
      setEmail('');
    },
  });
  const updateUser = trpc.users.update.useMutation({
    onSuccess: () => {
      users.refetch();
      setEditingId(null);
      setName('');
      setEmail('');
    },
  });
  const deleteUser = trpc.users.delete.useMutation({
    onSuccess: () => {
      users.refetch();
    },
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (editingId) {
      updateUser.mutate({ id: editingId, name, email });
    } else {
      createUser.mutate({ name, email });
    }
  };

  const handleEdit = (user: any) => {
    setEditingId(user.id);
    setName(user.name);
    setEmail(user.email);
  };

  const handleCancelEdit = () => {
    setEditingId(null);
    setName('');
    setEmail('');
  };

  return (
    <div className="container mx-auto p-8">
      <h1 className="text-3xl font-bold mb-8">tRPC + Drizzle + Supabase CRUD</h1>
      
      <div className="grid grid-cols-1 md:grid-cols-2 gap-8">
        <div>
          <h2 className="text-xl font-semibold mb-4">
            {editingId ? 'Edit User' : 'Create New User'}
          </h2>
          
          <form onSubmit={handleSubmit} className="space-y-4">
            <div>
              <label className="block text-sm font-medium mb-1">Name</label>
              <input
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                className="w-full p-2 border rounded"
                required
              />
            </div>
            
            <div>
              <label className="block text-sm font-medium mb-1">Email</label>
              <input
                type="email"
                value={email}
                onChange={(e) => setEmail(e.target.value)}
                className="w-full p-2 border rounded"
                required
              />
            </div>
            
            <div className="flex space-x-2">
              <button
                type="submit"
                disabled={createUser.isPending || updateUser.isPending}
                className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:opacity-50"
              >
                {editingId ? 'Update' : 'Create'} User
              </button>
              
              {editingId && (
                <button
                  type="button"
                  onClick={handleCancelEdit}
                  className="bg-gray-500 text-white px-4 py-2 rounded hover:bg-gray-600"
                >
                  Cancel
                </button>
              )}
            </div>
          </form>
        </div>

        <div>
          <h2 className="text-xl font-semibold mb-4">Users List</h2>
          
          {users.isPending && <p>Loading users...</p>}
          {users.isError && <p className="text-red-500">Error loading users</p>}
          
          {users.data && (
            <div className="space-y-2">
              {users.data.map((user) => (
                <div
                  key={user.id}
                  className="p-4 border rounded bg-gray-50 flex justify-between items-center"
                >
                  <div>
                    <h3 className="font-medium">{user.name}</h3>
                    <p className="text-gray-600">{user.email}</p>
                    <p className="text-xs text-gray-500">
                      Created: {new Date(user.createdAt).toLocaleDateString()}
                    </p>
                  </div>
                  
                  <div className="flex space-x-2">
                    <button
                      onClick={() => handleEdit(user)}
                      className="bg-yellow-500 text-white px-3 py-1 rounded text-sm hover:bg-yellow-600"
                    >
                      Edit
                    </button>
                    <button
                      onClick={() => deleteUser.mutate({ id: user.id })}
                      disabled={deleteUser.isPending}
                      className="bg-red-500 text-white px-3 py-1 rounded text-sm hover:bg-red-600 disabled:opacity-50"
                    >
                      Delete
                    </button>
                  </div>
                </div>
              ))}
              
              {users.data.length === 0 && (
                <p className="text-gray-500">No users found. Create your first user!</p>
              )}
            </div>
          )}
        </div>
      </div>
    </div>
  );
}
tRPC client function for working with front-end React components

That is all there is to working with tRPC! You are all set and ready to go!

Simply integrate this boilerplate setup into your own projects and you should be good to go.


Common Mistakes to Avoid in Project Setup

You can use this boilerplate code as is, but often times, if you are starting out on your own, there might be some mistakes you might make.

Some common issues you might encounter are failing to export the AppRouter, missing key definitions that define input schemas, incorrect defined route paths, and so much more.

It is best to stick to a boilerplate format such as the one outlined in this article to expediate the setup process for your web application.

Conclusion

We looked at tRPC in detail. You get type safety at each point of communication between the front-end and the back-end.

No need to worry about creating custom data types to handle fetched data.

Simply define your input types in a procedure function and validate it using Zod.

All your type needs are taken care of this way. We built on knowledge of a previous tutorial and used Drizzle to hook the back-end to a database and process database CRUD operations.

In the list below, you will find links to the GitHub repository, Drizzle docs, tRPC docs, and the TanStack Query docs:

I hope you found this article helpful and look forward to more in the future.

Thank you!

No Name

Abdullah Muhammad

Blogger. Software Engineer. Designer.

Subscribe to the newsletter

Get new articles, code samples, and project updates delivered straight to your inbox.