Setup project with Next.js, Prisma, tRPC, & NextAuth
Tags: nextjs, prisma, typescript, nextauth
Published: 2022-04-15 17:04:20

## 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](https://gdpd.xyz/) 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 template](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/n8gosd941p2jj9jk87dm.png)Link 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
```
**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
```

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

```typescript
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"
```
Copy the above import statements into the `context.ts` file then copy the below section of code.

```typescript
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>;
```
**backend/utils/createRouter**
Copy the below code into the `createRouter.ts` file

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

export function createRouter() {
    return trpc.router<Context>()
}
```
**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.

```typescript
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;
```
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.

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

```typescript
// 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 }
        }
    })
```
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**

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

export const trpc = createReactQueryHooks<AppRouter>();
```

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

```typescript
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]>;
```

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

```typescript
// 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);
```
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.

```react
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;
```
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](https://github.com/TheoBr/roundest-mon)

## Useful Links

- tRPC - https://trpc.io/
- Vercel - https://vercel.com/
- Prisma - https://www.prisma.io/
- NextJS - https://nextjs.org/
- Railway - https://railway.app/
- Typescript - https://www.typescriptlang.org/
- NextAuth - https://next-auth.js.org/
- Tailwind CSS - https://tailwindcss.com/