How to use Prisma ORM with Next.js
Introduction
This guide shows you how to use Prisma with Next.js 15, a fullstack React framework. You'll learn how to create a Prisma Postgres instance, 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
- Node.js 18+
- 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 you will be using for this guide.
npx create-next-app@latest nextjs-prisma
You will be prompted to answer a few questions about your project. Select all of the defaults.
For reference, those are:
- TypeScript
- ESLint
- Tailwind CSS
- No
src
directory - App Router
- Turbopack
- No customized import alias
Then, navigate to the project directory:
cd nextjs-prisma
2. Install and Configure Prisma
2.1. Install dependencies
To get started with Prisma, you'll need to install a few dependencies:
- Prisma Postgres (recommended)
- Other databases
npm install prisma tsx --save-dev
npm install @prisma/extension-accelerate @prisma/client
npm install prisma tsx --save-dev
npm install @prisma/client
Once installed, initialize Prisma in your project:
npx prisma init --db --output ../app/generated/prisma
You'll need to answer a few questions while setting up your Prisma Postgres database. Select the region closest to your location and a memorable name for your database like "My __________ Project"
This will create:
- A
prisma
directory with aschema.prisma
file. - A Prisma Postgres database.
- A
.env
file containing theDATABASE_URL
at the project root. - An
output
directory for the generated Prisma Client asapp/generated/prisma
.
2.2. Define your Prisma Schema
In the prisma/schema.prisma
file, add the following models:
generator client {
provider = "prisma-client-js"
output = "../app/generated/prisma"
}
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 creates two models: User
and Post
, with a one-to-many relationship between them.
2.3. Configure the Prisma Client generator
Now, run the following command to create the database tables and generate the Prisma Client:
npx prisma migrate dev --name init
2.4. Seed the database
Add some seed data to populate the database with sample users and posts.
Create a new file called seed.ts
in the prisma/
directory:
import { PrismaClient, Prisma } from "../app/generated/prisma";
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, tell Prisma how to run this script by updating your package.json
:
{
"name": "nextjs-prisma",
"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.7.0",
"@prisma/extension-accelerate": "^1.3.0",
"next": "15.3.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.1",
"prisma": "^6.7.0",
"tailwindcss": "^4",
"tsx": "^4.19.4",
"typescript": "^5"
}
}
Before starting the development server, note that if you are using Next.js v15.2.0 or v15.2.1, do not use Turbopack as there is a known issue. Remove Turbopack from your dev script by updating your package.json
"script":{
"dev": "next dev --turbopack",
"dev": "next dev",
}
This change is not needed on any versions before or after.
Finally, run prisma db seed
to seed your database with the initial data we defined in the seed.ts
file.
Run the seed script:
npx prisma db seed
And open Prisma Studio to inspect your data:
npx prisma studio
2.5 Set up Prisma Client
Now that you have a database with some initial data, you can set up Prisma Client and connect it to your 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:
- Prisma Postgres (recommended)
- Other databases
import { PrismaClient } from '../app/generated/prisma'
import { withAccelerate } from '@prisma/extension-accelerate'
const globalForPrisma = global as unknown as {
prisma: PrismaClient
}
const prisma = globalForPrisma.prisma || new PrismaClient().$extends(withAccelerate())
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
export default prisma
import { PrismaClient } from '../src/app/generated/prisma'
const globalForPrisma = global as unknown as {
prisma: PrismaClient
}
const prisma = globalForPrisma.prisma || new PrismaClient()
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.
You'll use this client in the next section to run your first queries.
3. Query your database with Prisma ORM
Now that you have an initialized Prisma Client, a connection to your database, and some initial data, you can start querying your data with Prisma ORM.
In this example, you'll make the "home" page of your application display all of your users.
Open the app/page.tsx
file and replace the existing code with the following:
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 you a basic page with a title and a list of users. However, that list is static with hardcoded values. Let's update the page to fetch the users from your database and make it dynamic.
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>
);
}
You are now importing your 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
You have your home page working, but you should add a new page that displays all of your 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:
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 hardcoded again. Let's update it to be dynamic, similarly to the home page:
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">
{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 you've used include
in your Prisma Client query to fetch the author of each post so you can display the author's name.
This "list view" is one of the most common patterns in web applications. You're going to add two more pages to your 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, you'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 your other pages, add the following code to the app/posts/new/page.tsx
file:
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 with hardcoded content. Let's update it to be dynamic based on the params
passed to the page:
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]">{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:
- You're using Prisma Client to fetch the post by its
id
, which you get from theparams
object. - In case the post doesn't exist (maybe it was deleted or maybe you typed a wrong ID), you call
notFound()
to display a 404 page. - You then display the post's title, content, and author. If the post doesn't have content, you 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 your application, you'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, you'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:
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:
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.
You 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 you deploy, you 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.
{
"name": "nextjs-prisma",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"postinstall": "prisma generate --no-engine",
"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"
}
}
If you're not using Prisma Postgres, you need to remove the --no-engine
flag from the command.
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:
Stay connected with Prisma
Continue your Prisma journey by connecting with our active community. Stay informed, get involved, and collaborate with other developers:
- Follow us on X for announcements, live events and useful tips.
- Join our Discord to ask questions, talk to the community, and get active support through conversations.
- Subscribe on YouTube for tutorials, demos, and streams.
- Engage on GitHub by starring the repository, reporting issues, or contributing to an issue.