Building modern full-stack applications just got easier with this powerful combination of tools
Hey developers! 👋 Ready to supercharge your Next.js 15 application with a robust database setup? Today we're diving deep into integrating Prisma ORM with Supabase's PostgreSQL database, using PNPM as our package manager. By the end of this guide, you'll have a rock-solid foundation for your next project.
Why This Stack Rocks
Before we jump into the code, let's talk about why this combination is absolutely fantastic:
- Next.js 15: The latest and greatest React framework with improved performance and developer experience
- Prisma: Type-safe database access with incredible developer tooling
- Supabase: Open-source Firebase alternative with a powerful PostgreSQL database
- PNPM: Lightning-fast, disk space efficient package manager that's becoming the new standard
This stack gives you type safety from database to frontend, excellent performance, and a delightful developer experience. Let's get started!
Prerequisites
Before we begin, make sure you have:
- Node.js 18+ installed
- PNPM installed globally (
npm install -g pnpm
) - A Supabase account (free tier works perfectly)
- Your favorite code editor ready to go
Step 1: Setting Up Your Next.js 15 Project
Let's start by creating a fresh Next.js 15 project using PNPM:
pnpm create next-app@latest my-prisma-supabase-app
cd my-prisma-supabase-app
When prompted, choose your preferred options. For this guide, I recommend:
- TypeScript: Yes
- ESLint: Yes
- Tailwind CSS: Yes (optional, but great for styling)
- App Router: Yes
- Import alias: Yes
Step 2: Installing Prisma and Dependencies
Now let's install all the packages we need:
# Install Prisma CLI and client
pnpm add prisma @prisma/client
# Optional install additional dependencies for our setup
pnpm add @supabase/supabase-js dotenv
Pro tip: PNPM's speed really shines here compared to npm or yarn!
Step 3: Setting Up Supabase
Head over to supabase.com and create a new project:
- Click "New Project"
- Choose your organization
- Enter a project name
- Set a database password (keep it safe!)
- Choose your region
- Click "Create new project"
Wait for Supabase to set up your database (usually takes 1-2 minutes). Once ready, grab your connection details:
- Go to Settings → Database
- Copy the connection string under "Connection string"
- Also note down your project URL and anon key from Settings → API
Step 4: Environment Configuration
Create a .env.local
file in your project root:
# Database connection string from Supabase
DATABASE_URL="postgresql://postgres:YOUR_PASSWORD@YOUR_PROJECT_REF.supabase.co:5432/postgres?pgbouncer=true&connection_limit=1"
# For direct connection (migrations)
DIRECT_URL="postgresql://postgres:YOUR_PASSWORD@YOUR_PROJECT_REF.supabase.co:5432/postgres"
# Supabase public configuration
NEXT_PUBLIC_SUPABASE_URL="https://YOUR_PROJECT_REF.supabase.co"
NEXT_PUBLIC_SUPABASE_ANON_KEY="your_anon_key_here"
Important notes:
- Replace
YOUR_PASSWORD
,YOUR_PROJECT_REF
, and your actual keys - The
DATABASE_URL
uses pgbouncer for connection pooling - The
DIRECT_URL
is needed for migrations - Never commit your
.env.local
file to version control!
Step 5: Initializing Prisma
Initialize Prisma in your project:
pnpm prisma init
This creates:
- A
prisma
folder withschema.prisma
- Updates your
.env
file (you can replace it with your.env.local
content)
Step 6: Configuring Prisma Schema
Open prisma/schema.prisma
and update it:
// 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"
url = env("DATABASE_URL")
directUrl = env("DIRECT_URL")
}
// Example models - customize based on your needs
model User {
id String @id @default(cuid())
email String @unique
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
@@map("users")
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
authorId String
author User @relation(fields: [authorId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@map("posts")
}
Step 7: Creating and Running Your First Migration
Now for the exciting part - let's create our database schema:
# Create and apply your first migration
pnpm prisma migrate dev --name init
This command:
- Creates a new migration file in
prisma/migrations
- Applies the migration to your Supabase database
- Generates the Prisma client
You should see output confirming the migration was successful!
Step 8: Setting Up Prisma Client
Create a lib/prisma.ts
file to set up your Prisma client:
import { PrismaClient } from "@prisma/client";
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const prismaClientSingleton = () => {
return new PrismaClient();
};
export const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = prisma;
This setup prevents multiple Prisma client instances during development hot reloads.
Step 9: Creating Your First API Route
Let's create an API route to test everything works. Create app/api/users/route.ts
:
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'
export async function GET() {
try {
const users = await prisma.user.findMany({
include: {
posts: true,
},
})
return NextResponse.json(users)
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch users' },
{ status: 500 }
)
}
}
export async function POST(request: Request) {
try {
const { name, email } = await request.json()
const user = await prisma.user.create({
data: {
name,
email,
},
})
return NextResponse.json(user, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 500 }
)
}
}
Step 10: Testing Your Setup
Let's create a simple page to test our setup. Update app/page.tsx
:
'use client'
import { useState, useEffect } from 'react'
interface User {
id: string
name: string | null
email: string
posts: any[]
}
export default function Home() {
const [users, setUsers] = useState<User[]>([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => {
setUsers(data)
setLoading(false)
})
}, [])
const createTestUser = async () => {
const response = await fetch('/api/users', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'John Doe',
email: `johndoe@example.com`,
}),
})
if (response.ok) {
const newUser = await response.json()
setUsers([...users, newUser])
}
}
if (loading) return <div className="p-8">Loading...</div>
return (
<div className="p-8">
<h1 className="text-2xl font-bold mb-4">Prisma + Supabase + Next.js 15</h1>
<button
onClick={createTestUser}
className="bg-blue-500 text-white px-4 py-2 rounded mb-4 hover:bg-blue-600"
>
Create User
</button>
<div>
<h2 className="text-xl font-semibold mb-2">Users ({users.length})</h2>
{users.map(user => (
<div key={user.id} className="border p-3 mb-2 rounded">
<p><strong>Name:</strong> {user.name || 'No name'}</p>
<p><strong>Email:</strong> {user.email}</p>
<p><strong>Posts:</strong> {user.posts.length}</p>
</div>
))}
</div>
</div>
)
}
Start your development server:
pnpm dev
Visit http://localhost:3000
and test creating a user!
Working with Prisma Migrations
Here are the essential migration commands you'll use regularly:
Creating New Migrations
When you modify your schema:
# Create a new migration
pnpm prisma migrate dev --name add_user_profile
# Reset database (careful in production!)
pnpm prisma migrate reset
Viewing Migration Status
# See migration status
pnpm prisma migrate status
# View your data
pnpm prisma studio
Production Deployments
For production:
# Deploy pending migrations
pnpm prisma migrate deploy
# Generate Prisma client
pnpm prisma generate
Best Practices and Pro Tips
1. Schema Design
- Use meaningful model and field names
- Add database constraints where appropriate
- Consider indexing for frequently queried fields
- Use enums for predefined values
2. Migration Management
- Always review migration files before applying
- Use descriptive migration names
- Never edit migration files manually
- Keep migrations small and focused
3. Performance Optimization
- Use connection pooling (already configured with Supabase)
- Implement proper indexing
- Use
select
to limit returned fields - Consider using
findFirst
instead offindMany
when appropriate
4. Error Handling
try {
const user = await prisma.user.create({ data: userData })
return user
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
// Handle specific Prisma errors
if (error.code === 'P2002') {
throw new Error('Email already exists')
}
}
throw error
}
5. Type Safety
import type { User, Post } from '@prisma/client'
type UserWithPosts = User & {
posts: Post[]
}
Package.json Scripts
Add these helpful scripts to your package.json
:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"db:migrate": "prisma migrate dev",
"db:studio": "prisma studio",
"db:seed": "tsx prisma/seed.ts"
}
}
Troubleshooting Common Issues
Connection Issues
- Double-check your
DATABASE_URL
andDIRECT_URL
- Ensure your Supabase project is running
- Verify your IP is allowed (Supabase allows all by default)
Migration Errors
- Check if your schema changes are valid
- Ensure you're not removing required fields without proper migration steps
- Review the generated migration SQL
Type Errors
- Run
pnpm prisma generate
after schema changes - Restart your TypeScript server in your editor
- Check for naming conflicts
Next Steps
Congratulations! 🎉 You now have a solid foundation with Prisma, Supabase, and Next.js 15. Here's what you might want to explore next:
- Authentication: Add Supabase Auth for user management
- Real-time Features: Implement Supabase real-time subscriptions
- File Storage: Use Supabase Storage for file uploads
- Advanced Queries: Explore Prisma's powerful querying capabilities
- Testing: Set up unit tests for your database operations
- Deployment: Deploy to Vercel with proper environment variables
Wrapping Up
This powerful combination of technologies gives you everything needed to build modern, scalable applications. The type safety from Prisma, the reliability of PostgreSQL through Supabase, and the performance of Next.js 15 create an excellent developer experience.
The setup might seem like a lot initially, but once it's running, you'll appreciate the robust foundation it provides.
Happy coding, and feel free to reach out if you have questions!
Found this guide helpful? Share it with your fellow developers and let me know what you build with this stack!
Top comments (0)