Skip to main content

How to use Prisma ORM with Next.js

Introduction

This guide shows you how to use Prisma ORM with Next.js 15, a fullstack React framework. You'll learn how to set up Prisma ORM with Next.js, handle migrations, and deploy your application to Vercel. You can find a deployment-ready example on GitHub.

Prerequisites

Before starting this guide, make sure you have:

  • Node.js 18+ installed
  • A Prisma Postgres database (or any PostgreSQL database)
  • A Vercel account (if you want to deploy your application)

1. Set up your project

From the directory where you want to create your project, run create-next-app to create a new Next.js app that we will be using for this guide.

npx create-next-app@latest my-app --yes

Then, navigate to the project directory:

cd my-app

2. Set up Prisma ORM

2.1 Install Prisma ORM and create your first models

First, we need to install a few dependencies. At the root of your project in your terminal, run:

npm install prisma --save-dev
npm install tsx --save-dev
npm install @prisma/extension-accelerate
info

If you're not using a Prisma Postgres database, you won't need the @prisma/extension-accelerate package.

Then, run prisma init to initialize Prisma ORM in your project.

npx prisma init

This will create a new prisma directory in your project, with a schema.prisma file inside of it. The schema.prisma file is where you will define your database models.

The prisma init command also creates a .env file in your project root. This file is used to store your database connection string.

Next, let's add two models to your schema.prisma file. A User model and a Post model.

prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}

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

model User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
}

model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId Int
author User @relation(fields: [authorId], references: [id])
}

This represents a simple blog with users and posts. Each Post can have a User as an author and each User can have many Posts.

Now that we have a Prisma Schema and a model, let's connect to our Prisma Postgres database.

2.2 Save your database connection string

Now that you have your Prisma schema, you will need a database to apply your schema to.

I don't have a database yet

If you don't have a database yet, you can create a new one through the . Step-by-step instructions are available in our getting started guide.

When you have created your Prisma Postgres project, you'll get a DATABASE_URL and PULSE_API_KEY. Store these in your .env file.

.env
DATABASE_URL=[Your Database URL Here]
PULSE_API_KEY=[Your Pulse API Key Here]
info

If you're not using Prisma Postgres you won't need the PULSE_API_KEY here.

2.3 Update your database schema

warning

If you're connecting to a database with existing data, use the prisma db pull command and then skip to Set up Prisma Client.

Now that you've saved your database connection string, we can apply your schema to your database using the prisma migrate dev command.

npx prisma migrate dev --name init

This creates an initial migration creating the User and Post tables and applies that migration to your database.

Now, let's add some initial data to our database.

2.4 Seed your database

Prisma ORM has built-in support for seeding your database with initial data. To do this, you can create a new file called seed.ts in the prisma directory.

prisma/seed.ts
import { PrismaClient, Prisma } from '@prisma/client'

const prisma = new PrismaClient()

const userData: Prisma.UserCreateInput[] = [
{
name: 'Alice',
email: 'alice@prisma.io',
posts: {
create: [
{
title: 'Join the Prisma Discord',
content: 'https://pris.ly/discord',
published: true,
},
{
title: 'Prisma on YouTube',
content: 'https://pris.ly/youtube',
},
],
},
},
{
name: 'Bob',
email: 'bob@prisma.io',
posts: {
create: [
{
title: 'Follow Prisma on Twitter',
content: 'https://www.twitter.com/prisma',
published: true,
},
],
},
}
]

export async function main() {
for (const u of userData) {
await prisma.user.create({ data: u })
}
}

main()

Now, add the prisma.seed configuration to your package.json file.

package.json
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^6.2.1",
"@prisma/extension-accelerate": "^1.2.1",
"next": "15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.4",
"postcss": "^8",
"prisma": "^6.2.1",
"tailwindcss": "^3.4.1",
"tsx": "^4.19.2",
"typescript": "^5"
}
}

Finally, run prisma db seed to seed your database with the initial data we defined in the seed.ts file.

npx prisma db seed

We now have a database with some initial data! You can check out the data in your database by running prisma studio.

npx prisma studio

2.5 Set up Prisma Client

Now that we have a database with some initial data, we can set up Prisma Client and connect it to our database.

At the root of your project, create a new lib directory and add a prisma.ts file to it.

mkdir -p lib && touch lib/prisma.ts

Now, add the following code to your lib/prisma.ts file:

lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { withAccelerate } from '@prisma/extension-accelerate'

const prisma = new PrismaClient().$extends(withAccelerate())

const globalForPrisma = global as unknown as { prisma: typeof prisma }

if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma

export default prisma

This file creates a Prisma Client and attaches it to the global object so that only one instance of the client is created in your application. This helps resolve issues with hot reloading that can occur when using Prisma ORM with Next.js in development mode.

note

If you're not using Prisma Postgres, replace the line

const prisma = new PrismaClient().$extends(withAccelerate())

with

const prisma = new PrismaClient()

and remove the @prisma/extension-accelerate import.

We'll use this client in the next section to run your first queries.

3. Query your database with Prisma ORM

Now that we have an initialized Prisma Client, a connection to our database, and some initial data, we can start querying our data with Prisma ORM.

In our example, we'll be making the "home" page of our application display all of our users.

Open the app/page.tsx file and replace the existing code with the following:

app/page.tsx
export default async function Home() {
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)] text-[#333333]">
Superblog
</h1>
<ol className="list-decimal list-inside font-[family-name:var(--font-geist-sans)]">
<li className="mb-2">Alice</li>
<li>Bob</li>
</ol>
</div>
);
}

This gives us a basic page with a title and a list of users. However, that list is static and doesn't change. Let's update the page to fetch the users from our database and make it dynamic.

app/page.tsx
import prisma from '@/lib/prisma'

export default async function Home() {
const users = await prisma.user.findMany();
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)] text-[#333333]">
Superblog
</h1>
<ol className="list-decimal list-inside font-[family-name:var(--font-geist-sans)]">
{users.map((user) => (
<li key={user.id} className="mb-2">
{user.name}
</li>
))}
</ol>
</div>
);
}

We are now importing our client, querying the User model for all users, and then displaying them in a list.

Now your home page is dynamic and will display the users from your database.

3.1 Update your data (optional)

If you want to see what happens when data is updated, you could:

  • update your User table via a SQL browser of your choice
  • change your seed.ts file to add more users
  • change the call to prisma.user.findMany to re-order the users, filter the users, or similar.

Just reload the page and you'll see the changes.

4. Add a new Posts list page

We have our home page working, but we should add a new page that displays all of our posts.

First create a new posts directory in the app directory and create a new page.tsx file inside of it.

mkdir -p app/posts && touch app/posts/page.tsx

Second, add the following code to the app/posts/page.tsx file:

app/posts/page.tsx
import prisma from "@/lib/prisma";

export default async function Posts() {
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)] text-[#333333]">
Posts
</h1>
<ul className="font-[family-name:var(--font-geist-sans)] max-w-2xl space-y-4">
<li>My first post</li>
</ul>
</div>
);
}

Now localhost:3000/posts will load, but the content is static. Let's update it to be dynamic, similarly to the home page:

app/posts/page.tsx
import prisma from "@/lib/prisma";

export default async function Posts() {
const posts = await prisma.post.findMany({
include: {
author: true,
},
});

return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center -mt-16">
<h1 className="text-4xl font-bold mb-8 font-[family-name:var(--font-geist-sans)] text-[#333333]">
Posts
</h1>
<ul className="font-[family-name:var(--font-geist-sans)] max-w-2xl space-y-4">
<li>My first post</li>
{posts.map((post) => (
<li key={post.id}>
<span className="font-semibold">{post.title}</span>
<span className="text-sm text-gray-600 ml-2">
by {post.author.name}
</span>
</li>
))}
</ul>
</div>
);
}

This works similarly to the home page, but instead of displaying users, it displays posts. You can also see that we've used include in our Prisma Client query to fetch the author of each post so we can display the author's name.

This "list view" is one of the most common patterns in web applications. We're going to add two more pages to our application which you'll also commonly need: a "detail view" and a "create view".

5. Add a new Posts detail page

To complement the Posts list page, we'll add a Posts detail page.

In the posts directory, create a new [id] directory and a new page.tsx file inside of that.

mkdir -p app/posts/[id] && touch app/posts/[id]/page.tsx

This page will display a single post's title, content, and author. Just like our other pages, add the following code to the app/posts/new/page.tsx file:

app/posts/[id]/page.tsx
import prisma from "@/lib/prisma";

export default async function Post({ params }: { params: Promise<{ id: string }> }) {
return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center -mt-16">
<article className="max-w-2xl space-y-4 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-4xl font-bold mb-8 text-[#333333]">My first post</h1>
<p className="text-gray-600 text-center">by Anonymous</p>
<div className="prose prose-gray mt-8">
No content available.
</div>
</article>
</div>
);
}

As before, this page is static. Let's update it to be dynamic based on the params passed to the page:

app/posts/[id]/page.tsx
import prisma from "@/lib/prisma";
import { notFound } from "next/navigation";

export default async function Post({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const post = await prisma.post.findUnique({
where: { id: parseInt(id) },
include: {
author: true,
},
});

if (!post) {
notFound();
}

return (
<div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center -mt-16">
<article className="max-w-2xl space-y-4 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-4xl font-bold mb-8 text-[#333333]">My first post</h1>
<p className="text-gray-600 text-center">by Anonymous</p>
<div className="prose prose-gray mt-8">
No content available.
</div>
<h1 className="text-4xl font-bold mb-8 text-[#333333]">{post.title}</h1>
<p className="text-gray-600 text-center">by {post.author.name}</p>
<div className="prose prose-gray mt-8">
{post.content || "No content available."}
</div>
</article>
</div>
);
}

There's a lot of changes here, so let's break it down:

  • We're using Prisma Client to fetch the post by its id, which we get from the params object.
  • In case the post doesn't exist (maybe it was deleted or maybe you typed a wrong ID), we call notFound() to display a 404 page.
  • We then display the post's title, content, and author. If the post doesn't have content, we display a placeholder message.

It's not the prettiest page, but it's a good start. Try it out by navigating to localhost:3000/posts/1 and localhost:3000/posts/2. You can also test the 404 page by navigating to localhost:3000/posts/999.

6. Add a new Posts create page

To round out our application, we'll add a "create" page for posts. This will let you write your own posts and save them to the database.

As with the other pages, we'll start with a static page and then update it to be dynamic.

mkdir -p app/posts/new && touch app/posts/new/page.tsx

Now, add the following code to the app/posts/new/page.tsx file:

app/posts/new/page.tsx
import Form from "next/form";

export default function NewPost() {
async function createPost(formData: FormData) {
"use server";

const title = formData.get("title") as string;
const content = formData.get("content") as string;
}

return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create New Post</h1>
<Form action={createPost} className="space-y-6">
<div>
<label htmlFor="title" className="block text-lg mb-2">
Title
</label>
<input
type="text"
id="title"
name="title"
placeholder="Enter your post title"
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="content" className="block text-lg mb-2">
Content
</label>
<textarea
id="content"
name="content"
placeholder="Write your post content here..."
rows={6}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600"
>
Create Post
</button>
</Form>
</div>
);
}

This form looks good, but it doesn't do anything yet. Let's update the createPost function to save the post to the database:

app/posts/new/page.tsx
import Form from "next/form";
import prisma from "@/lib/prisma";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export default function NewPost() {
async function createPost(formData: FormData) {
"use server";

const title = formData.get("title") as string;
const content = formData.get("content") as string;

await prisma.post.create({
data: {
title,
content,
authorId: 1,
},
});

revalidatePath("/posts");
redirect("/posts");
}

return (
<div className="max-w-2xl mx-auto p-4">
<h1 className="text-2xl font-bold mb-6">Create New Post</h1>
<Form action={createPost} className="space-y-6">
<div>
<label htmlFor="title" className="block text-lg mb-2">
Title
</label>
<input
type="text"
id="title"
name="title"
placeholder="Enter your post title"
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<div>
<label htmlFor="content" className="block text-lg mb-2">
Content
</label>
<textarea
id="content"
name="content"
placeholder="Write your post content here..."
rows={6}
className="w-full px-4 py-2 border rounded-lg"
/>
</div>
<button
type="submit"
className="w-full bg-blue-500 text-white py-3 rounded-lg hover:bg-blue-600"
>
Create Post
</button>
</Form>
</div>
);
}

This page now has a functional form! When you submit the form, it will create a new post in the database and redirect you to the posts list page.

We also added a revalidatePath call to revalidate the posts list page so that it will be updated with the new post. That way everyone can read the new post immediately.

Try it out by navigating to localhost:3000/posts/new and submitting the form.

7. Deploy your application to Vercel (Optional)

The quickest way to deploy your application to Vercel is to use the Vercel CLI.

First, install the Vercel CLI:

npm install -g vercel

Then, run vercel login to log in to your Vercel account.

vercel login

Before we deploy, we also need to tell Vercel to make sure that the Prisma Client is generated. You can do this by adding a postinstall script to your package.json file.

package.json
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"postinstall": "prisma generate",
"start": "next start",
"lint": "next lint"
},
"prisma": {
"seed": "tsx prisma/seed.ts"
},
"dependencies": {
"@prisma/client": "^6.2.1",
"@prisma/extension-accelerate": "^1.2.1",
"next": "15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.1.4",
"postcss": "^8",
"prisma": "^6.2.1",
"tailwindcss": "^3.4.1",
"tsx": "^4.19.2",
"typescript": "^5"
}
}

After this change, you can deploy your application to Vercel by running vercel.

vercel

After the deployment is complete, you can visit your application at the URL that Vercel provides. Congratulations, you've just deployed a Next.js application with Prisma ORM!

8. Next steps

Now that you have a working Next.js application with Prisma ORM, here are some ways you can expand and improve your application:

  • Add authentication to protect your routes
  • Add the ability to edit and delete posts
  • Add comments to posts
  • Use Prisma Studio for visual database management

For more information and updates: