Fullstack Application using tRPC, Next.js, Tailwind and Prisma
Tags: typescript, tailwindcss, nextjs, prisma
Published: 2023-03-25 18:54:20

hai guys, so previously I just post about whether is it worth learning tRPC or not and I want to be shared the implementation of tRPC on next.js.

so before we code, we need to initialize the next.js application. open your terminal or CMD and type

```
npx create-next-app trpc-basic
```
notes: you can give any name of your application but in this tutorial, I name it 'trpc-basic'


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1kdgn8hdn5gc9ijxgtje.png)

after you type, you need to specify the application. you can follow the settings that I made above.

after that, we need to install several libraries that we need to build full-stack application

1. trpc
```
npm install @trpc/server @trpc/client @trpc/react-query @trpc/next @tanstack/react-query
```
2. prisma

```
npm install prisma --save-dev
```
3. zod (validation)

```
npm install zod  
```
4. superjson (superset of JSON)

```
npm i superjson
```

## Setup Back End

open the directory that generates by create-next-app, and you need to create a folder structure like below.

```
.
├── prisma  # <-- if prisma is added
│   └── [..]
├── src
│   ├── pages
│   │   ├── _app.tsx  # <-- add `withTRPC()`-HOC here
│   │   ├── api
│   │   │   └── trpc
│   │   │       └── [trpc].ts  # <-- tRPC HTTP handler
│   │   └── [..]
│   ├── server
│   │   ├── routers
│   │   │   ├── _app.ts  # <-- main app router
│   │   │   ├── post.ts  # <-- sub routers
│   │   │   └── [..]
│   │   ├── context.ts   # <-- create app context
│   │   └── trpc.ts      # <-- procedure helpers
│   └── utils
│       └── prisma.ts  # <-- your typesafe tRPC hooks
│       └── trpc.ts  # <-- your typesafe tRPC hooks
└── [..]
```
note: I just followed the folder structure from tRPC docs.

after that open utils/trpc.ts and type

```
import { AppRouter } from '@/server/routers/_app';
import { httpBatchLink, loggerLink } from '@trpc/client';
import { createTRPCNext } from '@trpc/next';
import type { inferProcedureInput, inferProcedureOutput } from '@trpc/server';
// ℹ️ Type-only import:
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html#type-only-imports-and-export
import superjson from 'superjson';

function getBaseUrl() {
  if (typeof window !== 'undefined') {
    return '';
  }
  // reference for vercel.com
  if (process.env.VERCEL_URL) {
    return `https://${process.env.VERCEL_URL}`;
  }

  // // reference for render.com
  if (process.env.RENDER_INTERNAL_HOSTNAME) {
    return `http://${process.env.RENDER_INTERNAL_HOSTNAME}:${process.env.PORT}`;
  }

  // assume localhost
  return `http://localhost:${process.env.PORT ?? 3000}`;
}

/**
 * A set of strongly-typed React hooks from your `AppRouter` type signature with `createReactQueryHooks`.
 * @link https://trpc.io/docs/react#3-create-trpc-hooks
 */
export const trpc = createTRPCNext<AppRouter>({
  config({ ctx }) {
    /**
     * If you want to use SSR, you need to use the server's full URL
     * @link https://trpc.io/docs/ssr
     */
    return {
      /**
       * @link https://trpc.io/docs/data-transformers
       */
      transformer: superjson,
      /**
       * @link https://trpc.io/docs/links
       */
      links: [
        // adds pretty logs to your console in development and logs errors in production
        loggerLink({
          enabled: (opts) =>
            process.env.NODE_ENV === 'development' ||
            (opts.direction === 'down' && opts.result instanceof Error),
        }),
        httpBatchLink({
          url: `${getBaseUrl()}/api/trpc`,
        }),
      ],
      /**
       * @link https://react-query.tanstack.com/reference/QueryClient
       */
      // queryClientConfig: { defaultOptions: { queries: { staleTime: 60 } } },
    };
  },
  /**
   * @link https://trpc.io/docs/ssr
   */
  ssr: false,
});
```
so let me explain this, the code above is called custom hooked for use of tRPC in your application and there are several configs for data transformer, logger, etc.

next step, open utils/prisma.ts and type

```
/**
 * @link https://prisma.io/docs/support/help-articles/nextjs-prisma-client-dev-practices
 */
import { PrismaClient } from '@prisma/client';

export const prisma: PrismaClient =
  (global as any).prisma || new PrismaClient();

if (process.env.NODE_ENV !== 'production') {
  (global as any).prisma = prisma;
}
```
this configuration is for the integration prisma client with your application so you can use it.

next step, you need to fill server/trpc.ts with code below

```
import { initTRPC } from "@trpc/server";
import superjson from "superjson";
import { Context } from "./context";

const t = initTRPC.context<Context>().create({
    transformer: superjson,
});

export const router = t.router;
export const procedure = t.procedure;

```
this configuration is for initializing tRPC with context. maybe I will explain about context, so context holds data that all of your tRPC procedures will have access to, and is a great place to put things like database connections.

after that, we need to config context everytime request from client. open server/context.ts

```

import * as trpc from '@trpc/server';
import * as trpcNext from '@trpc/server/adapters/next';
import { prisma } from '@/utils/prisma';

// create context based of incoming request
export const createContext = async (
  opts?: trpcNext.CreateNextContextOptions,
) => {
  return {
    req: opts?.req,
    prisma,
    post: prisma.post,
  };
};
export type Context = trpc.inferAsyncReturnType<typeof createContext>;
```

we just finished to initialize tRPC but we can't use it because we need initialize router. so, open router/_app.ts

```
import { router } from "../trpc";
import { postRouter } from "./post";

export const appRouter = router({
    post: postRouter,
})

export type AppRouter = typeof appRouter;
```
we create AppRouter to merge router that represent feature. (try to read SOLID principle for make a better code)

after we merge router, type in router/post.ts

```
import { z } from "zod";
import { router, procedure } from "../trpc";

export const postRouter = router({
    all: procedure.query(({ctx}) => {
        return ctx.post.findMany({
            orderBy: {
                createdAt: 'asc'
            }
        })
    }),
    add: procedure.input(
        z.object({
            title: z.string().min(5),
            description: z.string(),
            author: z.string(),
        }),
    )
    .output(z.object({
        title: z.string().min(5),
        description: z.string(),
        author: z.string(),
    }))
    .mutation(async ({ctx, input}) => {
        const todo = await ctx.post.create({
            data: input
        })

        return todo
    })
})
```

in this tutorial, we just add some features like getting all posts and adding posts into the database. but before we add the post from the client, we need to validate input with a library called Zod. If the post was success validate then we add the post into the database using the function create(). create() is a function from prisma library that makes it easy to push the data into the database.

next step, open pages/api/trpc/[trpc].ts and then type

```
import * as trpcNext from "@trpc/server/adapters/next";
import { appRouter } from "@/server/routers/_app";
import { createContext } from "@/server/context";

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);
        }
      },
})
```
with this config, we can access tRPC API in next.js and we add appRouter from the router that we created before so you can use it to get all posts and add posts.

after that, we need to make model for database. so, open prisma/schema.prisma and type

```
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = env("DATABASE_URL")
}

model Post {
  id          Int       @id @default(autoincrement())
  title       String    
  description String    
  author      String    
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
}

```
for more about prisma ORM you can read this. [Prisma schema](https://www.prisma.io/docs/concepts/components/prisma-schema).


so basically, we just add columns into tables like id, title, description, etc with data types and you can also edit database providers such as SQL, PostgreSQL, MongoDB and etc.

before we migrate the database, we need to create .env file in root folder and type

```
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema

# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings

DATABASE_URL=file:./posting-trpc.db"
```
notes: you can customize DATABASE_URL with your databases that you use.

next step, run this code in cmd or terminal (root folder)
```
prisma migrate dev --name init
```

after that, we can check migration on folder prisma/migrations. if it's not empty, database migrate was success.

## Setup Front End

we're gonna use tailwind for styling css. so, type in your cmd or terminal

```
npm install -D tailwindcss postcss autoprefixer
```

after all that run properly, type 

```
npx tailwindcss init -p
```
this code will initialize tailwind.config.js. open the file and add this code into content

```
"./src/**/*.{js,ts,jsx,tsx}",
```

next step, open styles/globals.css and then edit the css with this code

```
@tailwind base;
@tailwind components;
@tailwind utilities;
```
in this tutorial, i just use tailwind for css and didn't use css native

how server that we made can comunicate with front-end? we can use trpc utility that we made before and wrapped the _app.tsx. the code will be like this

```
import '@/styles/globals.css'
import { trpc } from '@/utils/trpc'
import type { AppType } from 'next/app'


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

export default trpc.withTRPC(App);
```

after that, we can use tRPC on views or pages file. in this case index.tsx. edit the index.tsx like down below.

```
import { trpc } from '@/utils/trpc'
import { Post } from '@/types/post';
import { useState } from 'react';

const ListItem = ({post}: {post: Post}) => {
  return (
    <tr key={post.id} className={`${post.id % 2 === 1 ? 'bg-gray-200' : null}`}>
        <td className='px-4 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-800'>{post.title}</td>
        <td className='px-4 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-800'>{post.description}</td>
        <td className='px-4 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-800'>{post.author}</td>
        <td className='px-4 py-3.5 text-sm font-normal text-left rtl:text-right text-gray-800'>{post.createdAt.toLocaleString()}</td>
    </tr>
  )
}

type alertProps = {
  error: any;
  type: string;
}
const Alert = ({error, type}: alertProps) => {
  if(error === ""){
    return null
  }
  return (
    <div className={`rounded-lg ${type === 'error' ? "bg-red-500" : null} text-white p-2`}>
        <p>{error['message']}</p>
      </div>
  )
}

export default function Home() {
  const utils = trpc.useContext()
  const [title, setTitle] = useState("");
  const [description, setDescription] = useState("");
  const [author, setAuthor] = useState("");
  const [errorMessage, setErrorMessage] = useState("");

  const allPost = trpc.post.all.useQuery(undefined, {
    staleTime: 3000,
  })

  const submitPost = () => {
    addPost.mutate({title, description, author})
  }

  const addPost = trpc.post.add.useMutation({
    async onMutate() {
      await utils.post.all.cancel();
    },

    async onError(error) {
      console.log('error: ',error.data)
      setErrorMessage(error.message)
    },
    
    async onSuccess({title, description, author}) {
      const post = allPost.data ?? [];
      
      utils.post.all.setData(undefined, [
        ...post, 
        {
          id: Math.random(),
          title, 
          description, 
          author,
          createdAt: new Date(),
          updatedAt: new Date(),
        }
      ]
      )
    }
  });

  return (
    <div>
      <Alert
        error={errorMessage}
        type={'error'}
      />
      <h1 className='text-center my-5 font-semibold text-2xl'>create new post</h1>
      <div className='container w-1/2 mx-auto'>
      <div className='flex flex-col gap-4 mb-3'>
        <label className='font-bold text-lg'>Title</label>
        <input onChange={(e) => setTitle(e.target.value)} className="border p-2 rounded-lg" type={'text'} placeholder={"title here..."} />
      </div>
      <div className='flex flex-col gap-4 mb-3'>
        <label className='font-bold text-lg'>description</label>
        <textarea onChange={(e) => setDescription(e.target.value)} className="border p-2 rounded-lg" placeholder={"description here..."}></textarea>
      </div>
      <div className='flex flex-col gap-4'>
        <label className='font-bold text-lg'>author</label>
        <input onChange={(e) => setAuthor(e.target.value)} className="border p-2 rounded-lg" type={'text'} placeholder={"author here..."} />
      </div>
      <div className='text-center'>
      <button onClick={submitPost} className='bg-green-400 text-white p-3 rounded-lg mt-3 active:bg-green-600'>Post now</button>
      </div>
      </div>
      <h1 className='text-center my-5 font-semibold text-2xl'>list post</h1>
      <table className="table-auto mx-auto mt-5 border">
        <thead className='border'>
        <tr>
          <th className="px-4 py-3.5 text-sm font-bold text-center rtl:text-right text-gray-800">Title</th>
          <th className="px-4 py-3.5 text-sm font-bold text-center rtl:text-right text-gray-800">Description</th>
          <th className="px-4 py-3.5 text-sm font-bold text-center rtl:text-right text-gray-800">Author</th>
          <th className="px-4 py-3.5 text-sm font-bold text-center rtl:text-right text-gray-800">Created At</th>
        </tr>
        </thead>
        <tbody>
        {
            allPost.data?.map((data) => {
                return (
                  <ListItem post={data} key={data.id} />
                )
            })
          }
        </tbody>
      </table>
    </div>
  )
};
```

okay, I will explain this code above, so basically we want to get the data from tRPC by initializing tRPC in views and then we access the function from the router we made (in this case object name called all). after that, we want to add the post from the input we created in function postSubmit({title, description, author}).

you notice that we use useMutation for real-time fetch data, so every time we load again the page, data will be fetched again and we didn't need to reload the page to see data changed.

I think that from me, I wish this tutorial will help you to understand tRPC and how to create full-stack using next.js. thanks, guys :)