Build Full Stack Apps With Next.JS and Prisma

Have you ever had trouble choosing the right tools for your full-stack web application? Well, you are not alone. In this blog post, we will show you how to build a full-stack web application with minimum configuration and using only one language, TypeScript. We will create a simple CRUD app using Next.JS for frontend/backend and Prisma ORM with PostgreSQL for database operations.

Used technologies

Prisma

It is a next-generation ORM for Node.JS with TypeScript support out of the box. It has large feature sets such as:

  • migrating tool (Prisma Migrate)
  • seeding support
  • unique schema-driven data modelling (Prisma Schema)
  • type-safe query builder with type generation (Prisma Client)
  • database management tool (Prisma Studio)
  • wide support of database management systems (Postgres, MySQL, MongoDB, AzureSQL, SQLite, etc.)

Next.JS

It is a React framework for building user interfaces. Nowadays, Next is one of the most popular frontend frameworks in the market due to its wide variety of tooling options including:

  • internationalization
  • image optimization
  • server-side rendering (SSR) or static-site generation (SSG) for rendering pages
  • built-in routing with file system
  • API routes for writing server-side logic
  • Middleware

Initialize the application

To start the development, we need to run a few important commands. First, we will set up a basic Next.JS app with Typescript, install the Prisma CLI and finally initialize a basic Prisma setup. If you like to skip these steps, please check the results in the repository. Please check the sources section at the end of the article for the link.

npx create-next-app@latest –typescript npm install prisma –save-dev npx prisma init

The npx prisma init command created two files:

  • schema.prisma: Created inside the prisma directory. We will cover its purpose later.
  • .env: Here we have to provide a database connection string for our database.

The Prisma Schema

The prisma.schema is the main configuration file for our Prisma project. It consists of three main parts:

  1. Data sources: Specify the details of the data sources Prisma should connect to (e.g. a PostgreSQL database)
  2. Generators: They specify what clients should generate based on the data model (e.g. Prisma Client)
  3. Data models: Specifies your application models (the shape of the data per data source) and their relations

Since we are building a small CRUD application to handle personal goals per user, the next step is to create the User and Goal models for it.

generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) email String @unique firstName String @map("first_name") lastName String @map("last_name") goals Goal[] @@map("user") // table name in the database } model Goal { id Int @id @default(autoincrement()) name String description String @db.VarChar(255) priority Priority user User? @relation(fields: [userId], references: [id]) userId Int? @map("user_id") @@map("goal") // table name in the database } enum Priority { LOW MEDIUM HIGH }

For further details about Prisma models, please, check the official Prisma site, where you will find detailed documentation about data modelling.

The only thing missing is to run the following command:

npx prisma migrate dev

This will generate a migration file and update our database.

Client-server communication

Our tables have been created. Now we have to fill them with actual data. The Next.JS built-in API routing system will help with that because, with it, we can easily write code that’s running on the server. The only thing remaining is to use the query builders. For that, we need to install and instantiate a Prisma Client with the following command and code snippet:

npm install @prisma/client

import { PrismaClient } from '@prisma/client' export const prisma = new PrismaClient()

Important note: The name of the PrismaClient is a bit misleading since we cannot use it on the client side. It must run on the server. Please avoid calling it directly inside lifecycle methods or side effects.

Let’s create the API endpoints on the server by creating the following files under the api directory and use the prisma provided query builders for CRUD operations inside them. If you don’t want to add an extra API layer, it will be fine to use the query builders with server-side rendering (SSR) and static-site generation (SSG).

  • POST /api/goals/create (create.ts):

const { name, description, priority } = req.body const user = await prisma.user.findFirst() const result = await prisma.goal.create({ data: { name, description, priority, user: { connect: { id: user?.id } } } })

  • GET /api/goals (goals.ts):

const goals = await prisma.goal.findMany()

  • GET /api/goals/{id} ([id].ts):

const goalId = parseInt(req.query.id as string) const goal = await prisma.goal.findUnique({ where: { id: goalId } })

  • PUT /api/goals/{id} ([id].ts):

const goalId = parseInt(req.query.id as string) const { name, description, priority } = req.body const goal = await prisma.goal.update({ where: { id: goalId }, data: { name, description, priority } })

  • DELETE /api/goals/{id} ([id].ts):

const goalId = parseInt(req.query.id as string) await prisma.goal.delete({ where: { id: goalId } })

Note: Please check the repository for the full code

Last, we have to call these endpoints on the pages. We will use server-side rendering (SSR) for fetching goals and client-side logic for deleting one, but the choice is up to you since Next.JS supports various rendering options. Create a goals.tsx file under the pages folder and write the followings there:

interface GoalsProps { goals: Goal[] } const Goals: NextPage<GoalsProps> = ({ goals }) => { const router = useRouter() const handleDelete = useCallback(async (goalId: number) => { try { const res = await fetch(`/api/goals/${goalId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json' } }) if (res.ok) { router.replace(router.asPath) } } catch (e) { console.error(e) } }, []) return ( <LayoutBase> <Grid container direction="column" spacing={2} sx={{ my: 4 }}> {goals.map((goal, index) => ( <GoalItem goal={goal} key={index} onDelete={handleDelete} /> ))} </Grid> </LayoutBase> ) } export const getServerSideProps: GetServerSideProps = async () => { const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/api/goals`) const data = await response.json() return { props: { goals: data } } }

Note: Check the repository for the other pages and components.

Conclusion

Now we have a fully functioning full-stack Next.JS application using Prisma for database operations. Next.JS & Prisma stack is a perfect option if you need a relatively small full-stack application without any complex logic, with minimum configuration, and using the same language for the server and client. However, if you need a more complex application, you should look for a better and more battle-hardened alternative.

Sources

Blog Posts

View more

Graceful termination of Nginx in K8s

Graceful termination of Nginx in K8s

React Native vs. React Web: What's the Difference and When to Use Them

React Native vs. React Web: What's the Difference and When to Use Them