Seeding a Database in Next.js: Populating Test Data with Prisma
Step 6 of 31 — Next.js Tutorial Series | Source code for this step
Commands in This Step
| Command | Purpose |
|---|---|
pnpm add bcryptjs | Install bcrypt for password hashing |
npx prisma generate | Generate the Prisma Client |
npx prisma db seed | Seed database with test data |
npx prisma studio | Open database GUI in browser |
npx prisma migrate reset | Reset, re-migrate, and re-seed |
npx prisma migrate dev --name <name> | Create and apply a migration |
npx prisma migrate deploy | Apply pending migrations (production) |
npx prisma db pull | Pull schema from live database |
What You Will Build
By the end of this step you will have 5 users and 15 blog posts in your database. You will understand how to write a seed script, why passwords must be hashed, and how to reset and re-run the seed whenever you need fresh data.
Goal: Run npx prisma studio and see real data in the User and Post tables.
Table of Contents
- The Problem: An Empty Database is Useless for Development
- Core Concept: What is Seeding?
- Core Concept: Password Hashing
- Install bcryptjs
- Write the Seed Script
- Understanding the Seed Script
- Run the Seed
- Verify in Prisma Studio
- Core Concept: Resetting the Database
- Core Concept: Evolving Your Schema in Real Projects
- Core Concept: When to Run
prisma generate - Summary & Key Takeaways
The Problem: An Empty Database is Useless for Development
After Step 5, your database has tables but no data. When you build the UI to display posts, you need posts to display. When you build the login page, you need users to log in with.
Manually adding data through Prisma Studio every time is tedious and error-prone. What you need is a script that populates the database with realistic sample data — automatically, repeatably, and consistently.
Core Concept: What is Seeding?
Seeding is the process of populating a database with initial data. A seed script is a program that:
- Connects to the database
- Creates a set of predefined records (users, posts, etc.)
- Disconnects when done
You run it once to populate the database, and again whenever you need to start fresh. Every developer on a team runs the same seed script to get the same starting data.
In Prisma, the seed script is configured in prisma.config.ts — we already added the seed property in Step 4:
migrations: {
path: 'prisma/migrations',
seed: 'tsx ./prisma/seed.ts',
},
This tells Prisma: "When I run npx prisma db seed, execute prisma/seed.ts using tsx."
Core Concept: Password Hashing
Before we write the seed script, you need to understand a critical security concept.
Never store plain-text passwords. If your database is ever compromised, attackers would have everyone's actual passwords. Instead, we store a hash — a one-way transformation of the password.
Plain text: "password123"
↓ hash function (bcrypt)
Hashed: "$2b$10$K7L1OJ45/4Y2nIvhRVpCe.FSmhDdWoXehVzJptJ/op0lSsvqNu9F6"
Key properties of hashing:
| Property | What it means |
|---|---|
| One-way | You cannot reverse a hash back to the original password |
| Deterministic | The same password always produces the same hash (with the same salt) |
| Salted | bcrypt adds random data (salt) so identical passwords produce different hashes |
| Slow on purpose | bcrypt is intentionally slow to make brute-force attacks impractical |
How login works with hashed passwords:
- User types "password123" in the login form
- Your server hashes "password123" using bcrypt
- Server compares the new hash with the stored hash
- If they match → user is authenticated
You never need to "decrypt" the password. You only compare hashes.
Install bcryptjs
We use bcryptjs — a pure JavaScript implementation of bcrypt that works everywhere (no native compilation needed).
Run:
pnpm add bcryptjs
bcryptjs ships its own TypeScript type definitions, so no separate @types package is needed.
Write the Seed Script
We will split the seed into two files: one for the raw data (users and posts) and one for the script logic (connecting, hashing, inserting).
Create the Seed Data File
Create prisma/seed-data.ts:
export const users = [
{ email: '[email protected]', name: 'Alice' },
{ email: '[email protected]', name: 'Bob' },
{ email: '[email protected]', name: 'Charlie' },
{ email: '[email protected]', name: 'Diana' },
{ email: '[email protected]', name: 'Edward' },
]
// Each post uses an author key (lowercase name) that maps to a user above.
// Charlie has no posts — intentionally, to test empty states.
export const posts: { title: string; content: string; published: boolean; authorKey: string }[] = [
// Alice's posts (2)
{
title: 'Getting Started with TypeScript and Prisma',
content:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce id erat a lorem tincidunt ultricies. Vivamus porta bibendum nulla vel accumsan.',
published: true,
authorKey: 'alice',
},
{
title: 'How ORMs Simplify Complex Queries',
content:
'Duis sagittis urna ut sapien tristique convallis. Aenean vel ligula felis. Phasellus bibendum sem at elit dictum volutpat.',
published: false,
authorKey: 'alice',
},
// Bob's posts (3)
{
title: 'Mastering Prisma: Efficient Database Migrations',
content:
'Ut ullamcorper nec erat id auctor. Nullam nec ligula in ex feugiat tincidunt. Cras accumsan vehicula tortor ut eleifend.',
published: true,
authorKey: 'bob',
},
{
title: 'Best Practices for Type Safety in ORMs',
content:
'Aliquam erat volutpat. Suspendisse potenti. Maecenas fringilla elit vel eros laoreet, et tempor sapien vulputate.',
published: true,
authorKey: 'bob',
},
{
title: 'TypeScript Utility Types for Database Models',
content:
'Donec ac magna facilisis, vestibulum ligula at, elementum nisl. Morbi volutpat eget velit eu egestas.',
published: false,
authorKey: 'bob',
},
// Diana's posts (4)
{
title: 'Exploring Database Indexes and Their Performance Impact',
content:
'Vivamus ac velit tincidunt, sollicitudin erat quis, fringilla enim. Aenean posuere est a risus placerat suscipit.',
published: true,
authorKey: 'diana',
},
{
title: 'Choosing the Right Database for Your TypeScript Project',
content:
'Sed vel suscipit lorem. Duis et arcu consequat, sagittis justo quis, pellentesque risus. Curabitur sed consequat est.',
published: false,
authorKey: 'diana',
},
{
title: 'Designing Scalable Schemas with Prisma',
content:
'Phasellus ut erat nec elit ultricies egestas. Vestibulum rhoncus urna eget magna varius pharetra.',
published: true,
authorKey: 'diana',
},
{
title: 'Handling Relations Between Models in ORMs',
content:
'Integer luctus ac augue at tristique. Curabitur varius nisl vitae mi fringilla, vel tincidunt nunc dictum.',
published: false,
authorKey: 'diana',
},
// Edward's posts (5)
{
title: 'Why TypeORM Still Has Its Place in 2025',
content:
'Morbi non arcu nec velit cursus feugiat sit amet sit amet mi. Etiam porttitor ligula id sem molestie, in tempor arcu bibendum.',
published: true,
authorKey: 'edward',
},
{
title: 'NoSQL vs SQL: The Definitive Guide for Developers',
content:
'Suspendisse a ligula sit amet risus ullamcorper tincidunt. Curabitur tincidunt, sapien id fringilla auctor, risus libero gravida odio, nec volutpat libero orci nec lorem.',
published: true,
authorKey: 'edward',
},
{
title: "Optimizing Queries with Prisma's Select and Include",
content:
'Proin vel diam vel nisi facilisis malesuada. Sed vitae diam nec magna mollis commodo a vitae nunc.',
published: false,
authorKey: 'edward',
},
{
title: 'PostgreSQL Optimizations Every Developer Should Know',
content:
'Nullam mollis quam sit amet lacus interdum, at suscipit libero pellentesque. Suspendisse in mi vitae magna finibus pretium.',
published: true,
authorKey: 'edward',
},
{
title: 'Scaling Applications with Partitioned Tables in PostgreSQL',
content:
'Cras vitae tortor in mauris tristique elementum non id ipsum. Nunc vitae pulvinar purus.',
published: true,
authorKey: 'edward',
},
]
This file is pure data — no database logic. Each post uses an authorKey (like 'alice') instead of a real user ID, since IDs are auto-generated and unknown ahead of time.
Intentional data variety:
| User | Posts | Published | Why |
|---|---|---|---|
| Alice | 2 | 1 yes, 1 no | Average user |
| Bob | 3 | 2 yes, 1 no | Active user |
| Charlie | 0 | — | Tests empty state (user with no posts) |
| Diana | 4 | 2 yes, 2 no | Active user with drafts |
| Edward | 5 | 4 yes, 1 no | Most active user |
Create the Seed Script
Create prisma/seed.ts:
import { PrismaClient } from '../app/generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import bcrypt from 'bcryptjs'
import { users, posts } from './seed-data'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
async function main() {
// Create users with hashed passwords
const createdUsers = await Promise.all(
users.map((user) =>
prisma.user.create({
data: {
email: user.email,
name: user.name,
password: bcrypt.hashSync('password123', 10),
},
})
)
)
// Map lowercase name → id for post assignment
const userIdByName: Record<string, string> = {}
for (const user of createdUsers) {
userIdByName[user.name!.toLowerCase()] = user.id
}
// Create posts with author references
await prisma.post.createMany({
data: posts.map((post) => ({
title: post.title,
content: post.content,
published: post.published,
authorId: userIdByName[post.authorKey],
})),
})
console.log(`Seeded ${createdUsers.length} users and ${posts.length} posts.`)
}
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
The script is now clean and focused on logic only — the data lives in a separate file that is easy to edit without touching the script itself.
Understanding the Seed Script
Connecting to the Database
import { PrismaClient } from '../app/generated/prisma/client'
import { PrismaPg } from '@prisma/adapter-pg'
import bcrypt from 'bcryptjs'
import { users, posts } from './seed-data'
const adapter = new PrismaPg({ connectionString: process.env.DATABASE_URL })
const prisma = new PrismaClient({ adapter })
The seed script runs outside of Next.js (it is executed by tsx directly), so we need to set up the Prisma Client manually:
- Import
PrismaClientfrom the generated output atapp/generated/prisma/client - Import
PrismaPg— the PostgreSQL adapter that connects Prisma to our database - Import the seed data from our separate data file
- Create an adapter with the database URL from our
.envfile - Create a Prisma Client instance using that adapter
Why do we need an adapter? Prisma Postgres uses a specific connection protocol. The
PrismaPgadapter translates between Prisma's query engine and the PostgreSQL wire protocol. In Step 7, we will create a similar setup for our Next.js application.
Creating Users
const createdUsers = await Promise.all(
users.map((user) =>
prisma.user.create({
data: {
email: user.email,
name: user.name,
password: bcrypt.hashSync('password123', 10),
},
})
)
)
prisma.user.create() inserts a new row into the User table. Notice:
- We use
prisma.user(lowercase) even though the model isUser(PascalCase) - The
dataobject matches the fields we defined in the schema - We do not set
id— it is auto-generated by@default(cuid()) bcrypt.hashSync('password123', 10)hashes the password with a cost factor of 10
We use .map() to loop over the users array from seed-data.ts, keeping the script clean regardless of how many users we add later.
What is the cost factor? The number 10 is the salt rounds — it controls how computationally expensive the hash is. Higher numbers are more secure but slower:
| Rounds | Time (approx.) |
|---|---|
| 10 | ~100ms |
| 12 | ~300ms |
| 14 | ~1s |
10 is a good default for most applications.
Promise.all([...]) runs all five create calls concurrently. This is faster than creating them one by one. The result is an array of the created users, each with a generated id.
Mapping User IDs
const userIdByName: Record<string, string> = {}
for (const user of createdUsers) {
userIdByName[user.name!.toLowerCase()] = user.id
}
We need user IDs to connect posts to their authors. Since IDs are auto-generated CUIDs, we cannot know them ahead of time. This mapping builds a lookup from lowercase name (matching the authorKey in our data) to the generated ID.
Creating Posts
await prisma.post.createMany({
data: posts.map((post) => ({
title: post.title,
content: post.content,
published: post.published,
authorId: userIdByName[post.authorKey],
})),
})
prisma.post.createMany() inserts multiple rows in a single database call. This is more efficient than calling prisma.post.create() 15 times.
We use .map() to transform each post from our data file — replacing the authorKey string with the actual authorId from the lookup we built.
Notice:
- We do not set
id— auto-incremented (1, 2, 3...) - We do not set
createdAt— defaults tonow() - We do not set
updatedAt— managed by@updatedAt
The main() Wrapper
main()
.then(async () => {
await prisma.$disconnect()
})
.catch(async (e) => {
console.error(e)
await prisma.$disconnect()
process.exit(1)
})
This pattern ensures the database connection is always closed, even if an error occurs:
- On success — disconnect cleanly
- On error — log the error, disconnect, and exit with code
1(signals failure to the terminal)
Leaving database connections open can cause resource leaks, especially in development where you run scripts frequently.
Run the Seed
Important: The seed script imports the Prisma Client from
app/generated/prisma/client. This file is auto-generated by Prisma. Before running the seed, you must generate the client:npx prisma generate
Now run the seed script:
npx prisma db seed
You should see:
Loaded Prisma config from prisma.config.ts.
Running seed command `tsx ./prisma/seed.ts` ...
Seeded 5 users and 14 posts.
🌱 The seed command has been executed.
If you see Seeded 5 users and 14 posts. — your database is populated and ready.
Common Errors
"Cannot find module '../app/generated/prisma/client'" — Run npx prisma generate first to generate the client.
"Unique constraint failed on the fields: (email)" — You ran the seed twice without resetting. The seed tries to create users with the same emails. See the Resetting the Database section below.
Verify in Prisma Studio
Open Prisma Studio to see your data:
npx prisma studio
In your browser at http://localhost:5555:
- Click on User — you should see 5 rows (Alice, Bob, Charlie, Diana, Edward)
- Notice the
passwordcolumn — it shows hashed values like$2b$10$K7L1OJ45..., not "password123" - Click on Post — you should see 15 rows with titles, content, and author IDs
- Notice
idvalues are 1, 2, 3... (auto-incremented) - Notice
createdAthas timestamps — set automatically
Prisma Studio won't start? It requires a direct database connection (
postgres://...), not an accelerated URL. Make sure yourDATABASE_URLin.envis the direct URL (see Step 4).
Press Ctrl+C in the terminal to stop Prisma Studio when you are done.
Core Concept: Resetting the Database
During development, you will often want to start fresh — wipe all data and re-seed. Prisma provides a command for this:
npx prisma migrate reset
This command:
- Drops all tables in the database (deletes all data)
- Re-applies all migrations (recreates the tables)
- Runs the seed script (repopulates with sample data)
It will ask for confirmation before proceeding because it deletes everything.
When to use
migrate reset:
- Your data is in a bad state and you want to start fresh
- You changed the seed script and want to see the new data
- A teammate changed the schema and you need to rebuild
When NOT to use it:
- In production — this deletes all real data
- When you just want to add a new migration — use
npx prisma migrate devinstead
Core Concept: Evolving Your Schema in Real Projects
In a real project, your data model will change over time. You might need to:
- Add a new field to
User(likebio: String?) - Add a relationship to a new model (like
Comment) - Change an existing field type (like
email: String→email: Stringwith@unique) - Rename or delete fields
Here's how to handle schema evolution in different scenarios:
Scenario 1: Local Development (Safe to Reset)
You're developing locally and have no real data to preserve.
Procedure:
- Edit
schema.prismawith your changes - Run:
Prisma will:npx prisma migrate dev --name add_bio_to_user- Detect what changed
- Create a migration file
- Apply it to your local database
- Regenerate the Prisma Client
- If you want to re-seed with updated data (because the schema changed):
npx prisma migrate reset
Example: Add a bio field to the User model
model User {
id String @id @default(cuid())
name String?
email String @unique
password String
bio String? // ← New field
posts Post[]
}
Then:
npx prisma migrate dev --name add_bio_to_user
Prisma generates SQL like:
ALTER TABLE "User" ADD COLUMN "bio" TEXT;
Scenario 2: Staging/Production (Data is Valuable)
You have real data that must be preserved. You cannot use migrate reset.
Procedure:
- Edit
schema.prismawith your changes - Run:
This creates a migration on your local machinenpx prisma migrate dev --name your_change_name - Review the migration file in
prisma/migrations/<timestamp>_your_change_name/migration.sql— ensure it does what you expect - Commit and deploy:
- Push the changes to Git (including the new migration file)
- Deploy to staging/production
- Run on the server:
This applies pending migrations without re-seedingnpx prisma migrate deploy
migrate deployvsmigrate dev:
Command What it does When to use migrate devCreates a migration, applies it, regenerates client Local development migrate deployApplies existing migrations without creating new ones Staging/Production migrate resetDrops all tables, re-applies migrations, re-seeds Resetting local dev
Scenario 3: Destructive Changes (Rename or Delete Fields)
You want to rename or delete a field. Data will be lost.
Example: Rename bio → biography
- Edit schema:
model User { id String @id @default(cuid()) biography String? // ← Renamed from 'bio' } - Run:
npx prisma migrate dev --name rename_bio_to_biography - Review the generated migration. For a rename, Prisma might generate:
ALTER TABLE "User" RENAME COLUMN "bio" TO "biography";
Data loss warning: Deletions are permanent. Once you deploy a migration that removes a column, that data is gone. Always double-check before running
migrate deployin production.
Scenario 4: Adding a Relation to a New Model
You want to create a new Comment model related to Post.
Schema changes:
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
authorId String?
author User? @relation(fields: [authorId])
comments Comment[] // ← New relation
}
model Comment {
id Int @id @default(autoincrement())
text String
postId Int
post Post @relation(fields: [postId])
}
Commands:
npx prisma migrate dev --name add_comments
Prisma generates:
CREATE TABLE "Comment" (
"id" SERIAL NOT NULL,
"text" TEXT NOT NULL,
"postId" INTEGER NOT NULL,
CONSTRAINT "Comment_pkey" PRIMARY KEY ("id"),
CONSTRAINT "Comment_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id")
);
Quick Reference: What to Run When
| Situation | Command | Notes |
|---|---|---|
| Add a new field (local dev) | npx prisma migrate dev --name add_field | Safe, can reset after |
| Add a new field (production) | npx prisma migrate dev locally, then npx prisma migrate deploy on server | Data preserved |
| Delete/rename field | npx prisma migrate dev --name change_name | Review SQL carefully — data loss! |
| Forgot to run migrations? | npx prisma migrate deploy | Applies pending migrations |
| Fresh start (local only) | npx prisma migrate reset | Deletes everything, re-seeds |
| Add a related model | npx prisma migrate dev --name add_model_name | Creates new table + foreign keys |
| Update seeding logic | Edit seed-data.ts or seed.ts, then npx prisma migrate reset | Re-runs seed with new data |
Your project structure now
nextjs-16-crud/
├── prisma/
│ ├── schema.prisma
│ ├── seed.ts ← New: seed script (logic)
│ ├── seed-data.ts ← New: seed data (users & posts)
│ └── migrations/
│ ├── migration_lock.toml
│ └── 20260312XXXXXX_init/
│ └── migration.sql
├── app/
│ ├── generated/
│ │ └── prisma/
│ ├── layout.tsx
│ ├── Header.tsx
│ └── ...
├── prisma.config.ts
├── .env
└── package.json
Core Concept: When to Run prisma generate
This is the rule:
Whenever schema.prisma changes, Prisma Client may need to be regenerated so your app gets the new models, fields, relations, and TypeScript types.
The important detail is that sometimes Prisma does this automatically, and sometimes it does not.
When Prisma regenerates the client for you
If you run:
npx prisma migrate dev
Prisma will usually do all of this in one flow:
- Detect schema changes
- Create a migration
- Apply the migration to your local database
- Regenerate Prisma Client
So after migrate dev, you normally do not need to run npx prisma generate separately.
When you should run npx prisma generate yourself
Run it manually when the generated client might be out of date.
Common cases:
| Situation | Do you need npx prisma generate? | Why |
|---|---|---|
You changed schema.prisma and ran npx prisma migrate dev | Usually no | migrate dev regenerates the client |
You changed schema.prisma but have not run any Prisma command yet | Yes | Your code still uses the old generated client |
You ran npx prisma db pull | Yes | The schema changed from introspection, but your client must be rebuilt |
| You pulled someone else's schema changes from Git | Yes | Your local generated client may still reflect the old schema |
You deleted app/generated/prisma/client or cleaned build artifacts | Yes | The generated files must be recreated |
| TypeScript says a new field/model does not exist | Yes | Your code is using a stale generated client |
Practical examples
Example 1: Add a new field
You change:
model User {
id String @id @default(cuid())
name String?
email String @unique
password String
bio String?
}
If you then run:
npx prisma migrate dev --name add_bio_to_user
You are done. The migration runs and Prisma Client is regenerated automatically.
Example 2: Pull schema from the database
If you run:
npx prisma db pull
Prisma updates schema.prisma based on the database, but your generated client may still be old. After that, run:
npx prisma generate
How to think about it
schema.prisma is only the blueprint.
npx prisma generate turns that blueprint into the actual TypeScript client your app imports.
So if the blueprint changes but the generated client is not rebuilt, your code and schema are out of sync.
That is when you start seeing errors like:
Property 'bio' does not exist on type 'User'Cannot find module '../app/generated/prisma/client'- Autocomplete showing old fields instead of the new ones
Safe rule for beginners
If you are ever unsure, this is safe in local development:
npx prisma generate
It does not modify your database. It only regenerates the client files from your current schema.
Simple checklist
After changing schema.prisma, ask:
- Did I run
npx prisma migrate dev? - If not, did I at least run
npx prisma generate? - Does my code now recognize the new fields and models?
If the answer to step 2 is no, run:
npx prisma generate
Summary & Key Takeaways
| Concept | What it means |
|---|---|
| Seeding | Populating a database with initial data using a script |
prisma db seed | Runs the seed command defined in prisma.config.ts |
| Password hashing (bcrypt) | A one-way transformation that makes stored passwords unreadable |
| Salt rounds | Controls how slow (and secure) bcrypt hashing is — 10 is a good default |
prisma.user.create() | Inserts one row into a table and returns the created record |
prisma.post.createMany() | Inserts many rows in a single efficient database call |
PrismaPg adapter | Connects Prisma Client to a PostgreSQL database |
prisma migrate reset | Drops all tables, re-applies migrations, and re-runs the seed |
What is Next
In Step 7, we will create the Prisma Client singleton — a shared database connection that our Next.js application uses to query the database. This is the bridge between Prisma and your Next.js pages.