Building a Fullstack Next.js app -> tRPC(v10), Tailwind, Prisma
Published: 2023-03-18 15:05:34

I made this write-up as a follow up to Francisco Mendes' [post](https://dev.to/franciscomendes10866/build-a-full-stack-app-with-nextjs-tailwind-trpc-and-prisma-orm-4ail). While the post is a great way to get introduced to building with end-to-end type-safety in Nextjs, it was written on v9 of tRPC and as at this writing a lot has changed with v10. 

I'll cover the same app setup the only changes will be tRPC setup will follow the current v10(as at this writing) configurations. Let's get to it:

## What are the tools we'll use -> tRPC, Prisma, Zod

[tRPC](https://trpc.io/) - Is a toolkit that enables us to build totally type safe applications by only using inference. It's a great tool for building APIs and it's very easy to use.

[Prisma](https://www.prisma.io/) - is an ORM that enables us to write type safe queries to our database. It is a server-side library that helps developers read and write data to the database in an intuitive, efficient and safe way. It is easy to integrate into your framework of choice => Next.js, Graphql, Apollo, nest, Express e.t.c. Prisma simplifies database access, saves repetitive CRUD boilerplate and increases type safety. Its the perfect companion for building production-grade, robust and scalable web applications.

[Zod](https://zod.dev/) - is a TypeScript-first schema builder for data validation and parsing. It's a great tool for validating data before it gets to the database. It's very easy to use and it's very fast.

## Prerequisites

Of course you'll need basic knowledge of:
- Node
- Typescript
- Next.js
- Tailwind
- Npm

Okay, let's do this. Of course I'll add a link to the github repo at the bottom.

## Getting Started
### Next.js setup

We first spin up a next.js app and navigate to the project directory:
(note: when asked if you want `src` folder select `yes`. Also select `@` as `import alias`. this tutorial is setup that way)

```
npx create-next-app@latest --ts grocery-list
cd grocery-list
```

### Install and setup Tailwind CSS

```
npm install @fontsource/poppins
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
```

configure paths in the `@/tailwind.config.js`:

```js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx}",
    "./src/components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}
```

Add Tailwind directives to replace the styles in the `@/src/styles/globals.css`:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;

* {
    font-family: "Poppins";
  }
```

### Setup Prisma

Install Prisma and initialize it:

```
npm install prisma
npx prisma init
```

Edit the `schema.prisma` file as follows:
```ts
generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model GroceryList {
  id      Int      @id @default(autoincrement())
  title   String
  checked Boolean? @default(false)
}
```

Run prisma migration:
```
npx prisma migrate dev --name init

```

Now we can install prisma client:
Prisma Client is an auto-generated, type-safe query builder generated based on the models and attributes of your Prisma schema.

```
npm install @prisma/client

```

### Configure tRPC

Next.js makes it easy for you to build your client and server together in one codebase. tRPC makes it easy to share types between them, ensuring type-safety for your application's data fetching.

Install dependencies:
```
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query zod

```

> **@trpc/react-query**: provides a thin wrapper around @tanstack/react-query. It is required as a peer dependency as an idiomatic way to handle client-side caching in React applications.
> **Zod**: for input validation to ensure that our backend only processes requests that fit our API. You can use other validation libraries like Yup, Superstruct, io-ts if you prefer.

#### Enable strict mode

If you want to use Zod for input validation, make sure you have enabled strict mode in your `tsconfig.json`:

```json
"compilerOptions": {
   "strict": true
}
```

With dependencies installed we can create a `server` folder and a `context.ts` file to handle our context.

The context is used to pass contextual data to all router resolvers. In this case the context we pass is the prisma instance

```ts
// @/src/server/context.ts
import * as trpc from "@trpc/server"
import * as trpcNext from "@trpc/server/adapters/next"
import { PrismaClient } from "@prisma/client"

export async function createContext(opts?: trpcNext.CreateNextContextOptions) {
  const prisma = new PrismaClient()

  return { prisma }
}

export type Context = trpc.inferAsyncReturnType<typeof createContext>

```

#### Define our router

With our context created, we will now define our router and procedure helpers.

```ts
// @/src/server/router.ts
import { initTRPC } from '@trpc/server';
import { z } from "zod"

import { Context } from "./context"

const t = initTRPC.context<Context>().create();

// Base router and procedure helpers
const router = t.router;
const publicProcedure = t.procedure; 

export const serverRouter = router({
  findAll: publicProcedure
    .query(({ ctx }) => {
      return ctx.prisma.groceryList.findMany();
    }
  ),
  insertOne: publicProcedure
    .input(z.object({
        title: z.string(),
      })
    )
    .mutation(({ input, ctx }) => {
      return ctx.prisma.groceryList.create({
        data: { title: input.title },
      });
    }
  ),
  updateOne: publicProcedure
    .input(z.object({
        id: z.number(),
        title: z.string(),
        checked: z.boolean(),
    }))
    .mutation(({ input, ctx }) => {
      const { id, ...rest } = input;

      return ctx.prisma.groceryList.update({
        where: { id },
        data: { ...rest },
      });
    }
  ),
  deleteAll: publicProcedure
    .input(z.object({
        ids: z.number().array(),
    }))
    .mutation(({ input, ctx }) => {
      const { ids } = input;

      return ctx.prisma.groceryList.deleteMany({
        where: { id: { in: ids } },
      });
    }
  ),
});

export type ServerRouter = typeof serverRouter;

```

We export our `serverRouter` and its data type `ServerType`

> Note: Avoid exporting the entire t-object since it's not very descriptive. For instance, the use of a `t` variable is common in i18n libraries.

Now we need to create an API route from Next.js to which we will handle our handler api. We will pass our router and our context (which is invoked on every request)

```ts
// @/src/pages/api/trpc/[trpc].ts
import * as trpcNext from "@trpc/server/adapters/next"

import { serverRouter } from "@/server/router"
import { createContext } from "@/server/context"

export default trpcNext.createNextApiHandler({
  router: serverRouter,
  createContext,
});
```

Now we configure the `@/src/pages/_app.tsx` file

```ts
import '@/styles/globals.css'
import '@fontsource/poppins'
import type { AppType } from 'next/app';
import type { ServerRouter } from '@/server/router'
import { createTRPCNext } from '@trpc/next';
import { httpBatchLink } from '@trpc/client';

function getBaseUrl() {
  if (typeof window === 'undefined') {
    return process.env.VERCEL_URL
      ? `https://${process.env.VERCEL_URL}/api/trpc`
      : `http://localhost:3000/api/trpc`
  }
  return '/api/trpc'
}

const { withTRPC } = createTRPCNext<ServerRouter>({
  config({ ctx }) {
    const links = [
      httpBatchLink({
        url: getBaseUrl(),
      }),
    ];
    return { links };
  },
  ssr: true,
});

const App: AppType = ({ Component, pageProps }) => {
  return <Component {...pageProps} />
}

export default withTRPC(App);

```

Then we create a tPRC hook to which we will add the data type of our router as a generic on the `createTRPCReact()` function from react-query, so that we can make api calls:

```ts
// @/src/utils/trpc.ts
import { createReactQueryHooks, createTRPCReact } from "@trpc/react-query";
import type { ServerRouter } from "@/server/router";

export const trpc = createTRPCReact<ServerRouter>(); 

```

Now we are done with most of the work, lets build our Frontend.

### Frontend:

Lets put all the components in one folder `@/src/components/index.tsx`

```ts
import React, { memo } from 'react';
import type { NextPage } from 'next';
import { GroceryList } from '@prisma/client';

interface CardProps {
  children: React.ReactNode;
}

export const Card: NextPage<CardProps> = ({ children }) => {
  return (
    <div className="h-screen flex flex-col justify-center items-center bg-slate-100">
      {children}
    </div>
  );
};

export const CardContent: NextPage<CardProps>  = ({ children }) => {
  return (
    <div className="bg-white w-5/6 md:w-4/6 lg:w-3/6 xl:w-2/6 rounded-lg drop-shadow-md">
      {children}
    </div>
  );
};

interface CardHeaderProps {
  title: string;
  listLength: number;
  clearAllFn?: () => void;
}

export const CardHeader: NextPage<CardHeaderProps> = ({ 
  title, 
  listLength, 
  clearAllFn 
}) => {
  return (
    <div className="flex flex-row items-center justify-between p-3 border-b border-slate-200">
      <div className="flex flex-row items-center justify-between">
        <h1 className="text-base font-medium tracking-wide text-gray-900 mr-2">
          {title}
        </h1>
        <span className="h-5 w-5 bg-blue-200 text-blue-600 flex items-center justify-center rounded-full text-xs">
          {listLength}
        </span>
      </div>
      <button
        className="text-sm font-medium text-gray-600 underline"
        type='button'
        onClick={clearAllFn}
      >
        Clear All
      </button>
    </div>
  )
}

export const List: NextPage<CardProps> = ({ children }) => {
  return <div className="overflow-y-auto h-72">{children}</div>;
};

interface ListItemProps {
  item: GroceryList;
  onUpdate?: (item: GroceryList) => void;
}

const ListItemComponent: NextPage<ListItemProps> = ({ item, onUpdate }) => {
  return (
    <div className="h-12 border-b flex items-center justify-start px-3">
      <input
        type="checkbox"
        className="h-4 w-4 border-gray-300 rounded mr-4"
        defaultChecked={item.checked as boolean}
        onChange={() => onUpdate?.(item)}
      />
      <h2 className="text-gray-600 tracking-wide text-sm">{item.title}</h2>
    </div>
  )
}

export const ListItem = memo(ListItemComponent);

interface CardFormProps {
  value: string;
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
  submit: () => void;
}

export const CardForm: NextPage<CardFormProps> = ({ 
  value, 
  onChange, 
  submit 
}) => {
  return (
    <div className="bg-white w-5/6 md:w-4/6 lg:w-3/6 xl:w-2/6 rounded-lg drop-shadow-md mt-4">
      <div className="relative">
        <input
          type="text"
          className="w-full py-4 pl-3 pr-16 text-sm rounded-lg"
          placeholder="Grocery item name..."
          onChange={onChange}
          value={value}
        />
        <button
          type="button"
          className="absolute p-2 text-white -translate-y-1/2 bg-blue-600 rounded-full top-1/2 right-4"
          onClick={submit}
        >
          <svg
            className="w-4 h-4"
            xmlns="http://www.w3.org/2000/svg"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
          >
            <path
              strokeLinecap="round"
              strokeLinejoin="round"
              strokeWidth="2"
              d="M12 6v6m0 0v6m0-6h6m-6 0H6"
            />
          </svg>
        </button>
      </div>
    </div>
  )
}

```

We can now import the components and work on our main page `@/src/pages/index.tsx`

```ts
import Head from 'next/head'
import type { NextPage } from 'next'
import { useCallback, useState } from 'react'
import { trpc } from '@/utils/trpc'

import { 
  Card,
  CardContent,
  CardForm,
  CardHeader,
  List,
  ListItem,
} from '@/components'
import { GroceryList } from '@prisma/client'

const Home: NextPage = () => {
  const [itemName, setItemName] = useState<string>('');

  const { data: list, refetch} = trpc.findAll.useQuery();
  const insertMutation = trpc.insertOne.useMutation({
    onSuccess: () => {
      refetch();
    },
  });
  const deleteAllMutation = trpc.deleteAll.useMutation({
    onSuccess: () => {
      refetch();
    },
  });
  const updateOneMutation = trpc.updateOne.useMutation({
    onSuccess: () => {
      refetch();
    },
  });


  const insertOne = useCallback(() => {
    if (itemName === '') return;

    insertMutation.mutate({
      title: itemName,
    });
    setItemName('');
  }, [itemName, insertMutation]);

  const clearAll = useCallback(() => {
    if (list?.length) {
      deleteAllMutation.mutate({
        ids: list.map((item) => item.id),
      });
    }
  }, [deleteAllMutation, list]);

  const updateOne = useCallback(
    (item: GroceryList) => {
      updateOneMutation.mutate({
        ...item,
        checked: !item.checked,
      });
    },
    [updateOneMutation]
  );

  return (
    <>
      <Head>
        <title>Grocery List</title>
        <meta name="description" content="Grocery List" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <main>
        <Card>
          <CardContent>
            <CardHeader
              title="Grocery List"
              listLength={list?.length ?? 0}
              clearAllFn={clearAll}
            />
            <List>
              {list?.map((item) => (
                <ListItem key={item.id} item={item} onUpdate={updateOne} />
              ))}
            </List>
          </CardContent>
          <CardForm
            value={itemName}
            onChange={(e) => setItemName(e.target.value)}
            submit={insertOne}
          />
        </Card>
      </main>
    </>
  )
}

export default Home;

```

Final result should look like this:


![grocery-list](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zxu2pjmkj3f42gzxpx73.png)

_____

You can find all this in my repo [here](https://github.com/kharioki/grocery-list).

I hope this helps you get started with e2e type-safety.