DEV Community

Cover image for Setup project with Next.js, Prisma, tRPC, & NextAuth
Johannes Mogashoa
Johannes Mogashoa

Posted on • Edited on

Setup project with Next.js, Prisma, tRPC, & NextAuth

Background

Recently I was working on a small project where I could explore using technologies I haven't really used that much and put it all to practice. The aim was to build a clone of Kubool which is a platform that allows you to send anonymous messages. And so I got started.

My initial stack choice was:

  • NextJS
  • Prisma
  • Postgres from Railway
  • Typescript
  • NextAuth
  • Formik + Yup
  • Tailwind CSS
  • Vercel

I went ahead built the first basic version of the clone and deployed it on Vercel. You can check it out here: https://anony-sender.vercel.app. Later on I decided that "Nope let me rebuild the API endpoint to include tRPC which is a tool that provides you with end-to-end typesafe APIs. Adding tRPC to the project was a mission but I prevailed and thought why not share the steps of creating a project from scratch with tRPC. Links to the respective sites will be provided at the bottom.

Steps for setting up

The project setup will make use of the technologies I mentioned above. Prerequisites:

  • Node.JS
  • Text/code editor (I prefer VS Code)
  • Preferred command line tool (Windows Terminal)
  • Git

Step 1: Create & clone repo from starter template

As I have come to focus my interest in building projects with the above mentioned technologies, I went ahead and created a starter template for myself whenever beginning a new project. So let's build on top of that starter template.

Screenshot of the nextjs-prisma templateLink to starter template: https://github.com/JohannesMogashoa/nextjs-prisma

After creating and cloning your repo to your local machine. You may open up that project in your command line tool. Go ahead and install the dependencies by running npm install or yarn install

Step 2: Adding tRPC to the project

You can head over to the official tRPC site: https://trpc.io/ to take a look at their documentation.

At this point you can either use the command line tool or VS Code's integrated terminal.

yarn add @trpc/client @trpc/server @trpc/react @trpc/next zod react-query superjson ws
Enter fullscreen mode Exit fullscreen mode

Next Steps:

  • Create a backend folder in the root of your project
  • Create a utils and routers folder inside the backend folder
  • Inside the routers folder create an index.ts file
  • Inside the utils folder create a context.ts and createRouter.ts file
  • Inside the lib folder, create a trpc.ts file
  • Create a trp folder inside of pages/api/, then add a [trpc].ts file.

The paths to files mentioned above should be as follows:

- backend/routers/index.ts
- backend/utils/context.ts
- backend/utils/createRouter.ts
- lib/trpc.ts
- pages/api/trpc/[trpc].ts
Enter fullscreen mode Exit fullscreen mode

Step 3 - Set Up Backend Folders

backend/utils/context
Since the app is making use of prisma and nextauth, these "values" can be passed through to the rest of the tRPC routes using the context for easy accessibility throughout.

import { getSession } from "next-auth/react";
import * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import { NodeHTTPCreateContextFnOptions } from '@trpc/server/adapters/node-http';
import { IncomingMessage } from 'http';
import ws from 'ws';
import {prisma} from "@/lib/prisma"
Enter fullscreen mode Exit fullscreen mode

Copy the above import statements into the context.ts file then copy the below section of code.

export const createContext = async ({
    req,
    res,
}:
    | trpcNext.CreateNextContextOptions
    | NodeHTTPCreateContextFnOptions<IncomingMessage, ws>) => {
    const session = await getSession({ req });
    return {
        req,
        res,
        prisma,
        session,
    };
};

export type Context = trpc.inferAsyncReturnType<typeof createContext>;
Enter fullscreen mode Exit fullscreen mode

backend/utils/createRouter
Copy the below code into the createRouter.ts file

import { Context } from "./context";
import * as trpc from "@trpc/server";

export function createRouter() {
    return trpc.router<Context>()
}
Enter fullscreen mode Exit fullscreen mode

backend/routers/index
In this file, the tRPC App Router is created with all the different routes you would like to have. The below example is if you would like to have ALL your routes in the same file.

import { createRouter } from '@/backend/utils/createRouter';
import superjson from "superjson"
import { z } from 'zod';

export const appRouter = createRouter()
    .transformer(superjson)
    .mutation('set-username', {
        input: z.object({
            username: z.string(),
            email: z.string().email()
        }),
        async resolve({ ctx, input }) {

            await ctx.prisma!.user.update({
                where: {
                    email: input.email,
                },
                data: {
                    slug: input.username,
                },
            })

            return { success: true, message: "Username set successfully" }
        }
    })
    .query('get-user', {
        input: z.object({
            email: z.string().email()
        }),
        async resolve({ ctx, input }) {
            const user = await ctx.prisma!.user.findUnique({
                where: {
                    email: input.email,
                },
            })

            return { success: true, user }
        }
    })

// export type definition of API
export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode

As you can imagine, the above code will become more and more as you add other routes and their respective logic. So a better way would be to group routes and place in another file then import those routes into the appRouter. Create another file i.e. userRoutes.ts inside the backend/routers folder. The refactored code would like something of the below nature.

// backend/routers/index.ts

import { createRouter } from '@/backend/utils/createRouter';
import superjson from "superjson"
import { userRouter } from './userRouter';

export const appRouter = createRouter()
    .transformer(superjson)
    .merge('user', userRouter)
// other merged routes here

// export type definition of API
export type AppRouter = typeof appRouter;
Enter fullscreen mode Exit fullscreen mode
// backend/routers/userRouter.ts

import { z } from "zod"
import { createRouter } from "../utils/createRouter"

export const userRouter = createRouter()
    .mutation('set-username', {
        input: z.object({
            username: z.string(),
            email: z.string().email()
        }),
        async resolve({ ctx, input }) {

            await ctx.prisma!.user.update({
                where: {
                    email: input.email,
                },
                data: {
                    slug: input.username,
                },
            })

            return { success: true, message: "Username set successfully" }
        }
    })
    .query('get-user', {
        input: z.object({
            email: z.string().email()
        }),
        async resolve({ ctx, input }) {
            const user = await ctx.prisma!.user.findUnique({
                where: {
                    email: input.email,
                },
            })

            return { success: true, user }
        }
    })
Enter fullscreen mode Exit fullscreen mode

When you have added all the above code consider yourself temporarily done with the backend/ folder and you can now move onto the other files in the other folders.

lib/trpc.ts

import { AppRouter } from '@/backend/routers';
import { createReactQueryHooks } from '@trpc/react';

export const trpc = createReactQueryHooks<AppRouter>();
Enter fullscreen mode Exit fullscreen mode

pages/api/trpc/[trpc].ts
This portion is the official setup of the tRPC API endpoint that will handle requests made to your defined routers in the App Router.

import { appRouter, AppRouter } from '@/backend/routers';
import { inferProcedureOutput } from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import { createContext } from '@/backend/utils/context';

// export API handler
export default trpcNext.createNextApiHandler({
    router: appRouter,
    createContext,
    onError({ error }) {
        if (error.code === 'INTERNAL_SERVER_ERROR') {
            // send to bug reporting
            console.error('Something went wrong', error);
        }
    },
});

export type inferQueryResponse<
    TRouteKey extends keyof AppRouter["_def"]["queries"]
    > = inferProcedureOutput<AppRouter["_def"]["queries"][TRouteKey]>;
Enter fullscreen mode Exit fullscreen mode

At this point you have completed adding the necessary code into all the files you have created. Now moving onto the final implementation of tRPC in the pages/_app.tsx file.

Step 4 - Edit pages/_app.tsx file

Navigate to the pages/_app.tsx file and make the following changes to it:

  • Remove the export default MyApp line and paste the following code
// Initializing TRPC server on the Next.js server
import { withTRPC } from "@trpc/next";
import type { AppRouter } from "@/backend/routers";

// Check to see the current environment then generate the appropriate URL
function getBaseUrl() {
    if (process.browser) return ""; // Browser should use current path
    if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url

    return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
}

export default withTRPC<AppRouter>({
    config({ ctx }) {
        /**
         * If you want to use SSR, you need to use the server's full URL
         * @link https://trpc.io/docs/ssr
         */
        const url = `${getBaseUrl()}/api/trpc`;

        return {
            url,
            /**
             * @link https://react-query.tanstack.com/reference/QueryClient
             */
            // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
        };
    },
    /**
     * @link https://trpc.io/docs/ssr
     */
    ssr: false,
})(MyApp);
Enter fullscreen mode Exit fullscreen mode

Phew!!!! That was a lot of code. At this point in time, you should be able to connect your "frontend" to your "backend" using the tRPC client instance created in your lib/trpc.ts file.

Step 5 - Using tRPC

Now that everything has been implemented on the backend, we can now use the client instance wherever we use to use it. Let's try getting a user from the database using tRPC's hooks that are using React Query under the hood.

import { trpc } from "@/utils/trpc";
import { useSession } from "next-auth/react";
import React from "react";

const UserComponent: React.FC = () => {
    const {data: session} = useSession()
    const {data} = trpc.useQuery(["get-user", { email: session?.user?.email as string}]);

    return (
        <div>
            <h1>{data?.user?.name}</h1>
            <p>{data?.user?.email}</p>
        </div>
    );
};

export default UserComponent;
Enter fullscreen mode Exit fullscreen mode

At this point, you can happily say you have implemented tRPC into your NextJS application.

Outro

Believe me or not, I had written and completed the app using Next's built-in API routing and handling system but the code was messy, had typecasting all over the place in order to mimic some sort of end-to-end type safety between the backend and the client. I have found tRPC to be a really useful tool and look forward to using it more often. If you would like to checkout a github repo that uses tRPC without NextAuth take a look at Roundest by Theo Browne

Useful Links

Top comments (1)