Skip to main content

Next.js Prisma ORM

Prisma ORM is a powerful database toolkit that makes database access easy with an auto-generated and type-safe query builder for TypeScript & Node.js. In this guide, we'll explore how to integrate Prisma with Next.js to create robust, type-safe database applications.

Introduction to Prisma

Prisma simplifies database workflows and replaces traditional ORMs with its modern data access approach. It consists of three main tools:

  1. Prisma Client: Auto-generated and type-safe query builder
  2. Prisma Migrate: Migration system for database schema changes
  3. Prisma Studio: GUI to view and edit data in your database

When used with Next.js, Prisma provides an excellent developer experience with features like:

  • Type safety for your database queries
  • Auto-completion in your IDE
  • Real-time feedback on database errors
  • Simple and intuitive API for database operations

Setting Up Prisma in a Next.js Project

Let's walk through the process of integrating Prisma with Next.js:

Step 1: Install necessary dependencies

bash
npm install prisma @prisma/client
npx prisma init

This initializes a new Prisma project with a prisma directory containing:

  • schema.prisma: Your database schema definition
  • .env: Environment variables file with your database connection URL

Step 2: Define your database schema

Edit the prisma/schema.prisma file to define your data models:

prisma
// 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 = "postgresql" // or "mysql", "sqlite", etc.
url = env("DATABASE_URL")
}

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

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

Step 3: Set up your database connection

In your .env file, add your database connection string:

DATABASE_URL="postgresql://username:password@localhost:5432/mydatabase?schema=public"

Replace the values with your actual database credentials.

Step 4: Generate Prisma Client

After defining your schema, generate the Prisma Client:

bash
npx prisma generate

This creates a tailored and type-safe database client based on your schema.

Step 5: Create database tables

To create the tables in your database based on your schema:

bash
npx prisma migrate dev --name init

This creates and applies a migration, setting up your database tables.

Using Prisma Client in Next.js

Now that everything is set up, let's create a utility file to use Prisma throughout your application:

Create a file lib/prisma.ts:

typescript
import { PrismaClient } from '@prisma/client'

// PrismaClient is attached to the `global` object in development to prevent
// exhausting your database connection limit.
// Learn more: https://pris.ly/d/help/next-js-best-practices

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

export const prisma = globalForPrisma.prisma || new PrismaClient()

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

export default prisma

This approach ensures that during development, we don't create a new Prisma Client instance each time the file is imported.

Practical Examples

Let's explore how to perform CRUD operations using Prisma in your Next.js API routes and server components.

Example 1: Fetching Data in a Server Component

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

export default async function UsersPage() {
const users = await prisma.user.findMany({
include: {
posts: true,
},
})

return (
<div>
<h1>Users</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Posts: {user.posts.length}</p>
</li>
))}
</ul>
</div>
)
}

Example 2: Creating an API Route to Add Data

typescript
// app/api/users/route.ts
import prisma from '@/lib/prisma'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
try {
const body = await request.json()
const { name, email } = body

const newUser = await prisma.user.create({
data: {
name,
email,
},
})

return NextResponse.json(newUser, { status: 201 })
} catch (error) {
console.error('Request error', error)
return NextResponse.json({ error: 'Error creating user' }, { status: 500 })
}
}

Example 3: Creating a Blog Post Form

Let's create a form to add new blog posts:

tsx
// app/create-post/page.tsx
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'

export default function CreatePost() {
const [title, setTitle] = useState('')
const [content, setContent] = useState('')
const [authorId, setAuthorId] = useState('')
const [isLoading, setIsLoading] = useState(false)
const router = useRouter()

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setIsLoading(true)

try {
const response = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
title,
content,
authorId: parseInt(authorId),
}),
})

if (response.ok) {
router.push('/posts')
router.refresh() // Refresh the page to show the new post
}
} catch (error) {
console.error('Error creating post:', error)
} finally {
setIsLoading(false)
}
}

return (
<div className="max-w-md mx-auto mt-10">
<h1 className="text-2xl font-bold mb-5">Create New Post</h1>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="title" className="block mb-1">Title</label>
<input
id="title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
required
/>
</div>

<div>
<label htmlFor="content" className="block mb-1">Content</label>
<textarea
id="content"
value={content}
onChange={(e) => setContent(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
rows={5}
/>
</div>

<div>
<label htmlFor="authorId" className="block mb-1">Author ID</label>
<input
id="authorId"
type="number"
value={authorId}
onChange={(e) => setAuthorId(e.target.value)}
className="w-full border border-gray-300 rounded px-3 py-2"
required
/>
</div>

<button
type="submit"
disabled={isLoading}
className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 disabled:bg-blue-300"
>
{isLoading ? 'Creating...' : 'Create Post'}
</button>
</form>
</div>
)
}

And the corresponding API route:

typescript
// app/api/posts/route.ts
import prisma from '@/lib/prisma'
import { NextResponse } from 'next/server'

export async function POST(request: Request) {
try {
const body = await request.json()
const { title, content, authorId } = body

const newPost = await prisma.post.create({
data: {
title,
content,
author: { connect: { id: authorId } },
},
})

return NextResponse.json(newPost, { status: 201 })
} catch (error) {
console.error('Request error', error)
return NextResponse.json({ error: 'Error creating post' }, { status: 500 })
}
}

Advanced Prisma Features with Next.js

Relationships and Nested Writes

Prisma makes it easy to work with related data:

typescript
// Creating a user with posts in a single query
const createUserWithPosts = await prisma.user.create({
data: {
name: 'Alice',
email: '[email protected]',
posts: {
create: [
{ title: 'Hello World', content: 'This is my first post!' },
{ title: 'My Second Post', content: 'More content here...' },
],
},
},
include: {
posts: true, // Include all posts in the returned object
},
})

Transactions

For operations that require multiple database changes, use transactions to ensure data integrity:

typescript
// Example of using transactions
const transferPostAuthorship = await prisma.$transaction(async (tx) => {
// Update the post to have a new author
const updatedPost = await tx.post.update({
where: { id: postId },
data: { authorId: newAuthorId },
})

// Update statistics for both authors
await tx.user.update({
where: { id: oldAuthorId },
data: { postCount: { decrement: 1 } },
})

await tx.user.update({
where: { id: newAuthorId },
data: { postCount: { increment: 1 } },
})

return updatedPost
})

Middleware Example

Prisma middleware allows you to add hooks to your queries. Here's how to add simple logging:

typescript
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// Log all queries in development
prisma.$use(async (params, next) => {
const before = Date.now()
const result = await next(params)
const after = Date.now()

console.log(`Query ${params.model}.${params.action} took ${after - before}ms`)

return result
})

export default prisma

Performance Optimization

Connection Pooling

When deploying to serverless environments like Vercel, you should use connection pooling to prevent exceeding database connection limits:

  1. Add a pooling service like PgBouncer to your database setup
  2. Modify your connection string to use the pooling service:
DATABASE_URL="postgresql://username:password@pooler:6432/mydatabase?schema=public"

Implementing Caching

To reduce database load, implement caching for frequently accessed data:

typescript
// utils/cache.ts
import { cache } from 'react'
import prisma from '@/lib/prisma'

export const getUsers = cache(async () => {
const users = await prisma.user.findMany()
return users
})

export const getPostsByAuthorId = cache(async (authorId: number) => {
const posts = await prisma.post.findMany({
where: { authorId },
orderBy: { createdAt: 'desc' },
})
return posts
})

Summary

In this guide, we've covered:

  1. Setting up Prisma with Next.js
  2. Creating a database schema and generating the Prisma Client
  3. Implementing CRUD operations using Prisma in Next.js
  4. Advanced features like relationships and transactions
  5. Performance optimization through connection pooling and caching

Prisma ORM provides a powerful, type-safe way to interact with your database in Next.js applications. Its intuitive API and robust feature set make it an excellent choice for projects of all sizes.

Additional Resources

Practice Exercises

  1. Basic: Create a CRUD API for a "Product" model with fields for name, price, and description.
  2. Intermediate: Implement a blog system with comments using Prisma relationships.
  3. Advanced: Create a dashboard that shows analytics about your database entries (number of users, posts per user, etc.) using Prisma aggregations.
  4. Challenge: Implement a full-stack application with authentication that uses Prisma for user management and session storage.

Happy coding with Prisma and Next.js!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)