Seeding a Database in Next.js: Populating Test Data with Prisma

Step 6 of 31Next.js Tutorial Series | Source code for this step

Live Demo →


Commands in This Step

CommandPurpose
pnpm add bcryptjsInstall bcrypt for password hashing
npx prisma generateGenerate the Prisma Client
npx prisma db seedSeed database with test data
npx prisma studioOpen database GUI in browser
npx prisma migrate resetReset, re-migrate, and re-seed
npx prisma migrate dev --name <name>Create and apply a migration
npx prisma migrate deployApply pending migrations (production)
npx prisma db pullPull schema from live database

What You Will Build

↑ Index

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

  1. The Problem: An Empty Database is Useless for Development
  2. Core Concept: What is Seeding?
  3. Core Concept: Password Hashing
  4. Install bcryptjs
  5. Write the Seed Script
  6. Understanding the Seed Script
  7. Run the Seed
  8. Verify in Prisma Studio
  9. Core Concept: Resetting the Database
  10. Core Concept: Evolving Your Schema in Real Projects
  11. Core Concept: When to Run prisma generate
  12. Summary & Key Takeaways

The Problem: An Empty Database is Useless for Development

↑ Index

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?

↑ Index

Seeding is the process of populating a database with initial data. A seed script is a program that:

  1. Connects to the database
  2. Creates a set of predefined records (users, posts, etc.)
  3. 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

↑ Index

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:

PropertyWhat it means
One-wayYou cannot reverse a hash back to the original password
DeterministicThe same password always produces the same hash (with the same salt)
Saltedbcrypt adds random data (salt) so identical passwords produce different hashes
Slow on purposebcrypt is intentionally slow to make brute-force attacks impractical

How login works with hashed passwords:

  1. User types "password123" in the login form
  2. Your server hashes "password123" using bcrypt
  3. Server compares the new hash with the stored hash
  4. If they match → user is authenticated

You never need to "decrypt" the password. You only compare hashes.


Install bcryptjs

↑ Index

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

↑ Index

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:

UserPostsPublishedWhy
Alice21 yes, 1 noAverage user
Bob32 yes, 1 noActive user
Charlie0Tests empty state (user with no posts)
Diana42 yes, 2 noActive user with drafts
Edward54 yes, 1 noMost 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

↑ Index

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:

  1. Import PrismaClient from the generated output at app/generated/prisma/client
  2. Import PrismaPg — the PostgreSQL adapter that connects Prisma to our database
  3. Import the seed data from our separate data file
  4. Create an adapter with the database URL from our .env file
  5. Create a Prisma Client instance using that adapter

Why do we need an adapter? Prisma Postgres uses a specific connection protocol. The PrismaPg adapter 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 is User (PascalCase)
  • The data object 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:

RoundsTime (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 to now()
  • 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

↑ Index

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

↑ Index

Open Prisma Studio to see your data:

npx prisma studio

In your browser at http://localhost:5555:

  1. Click on User — you should see 5 rows (Alice, Bob, Charlie, Diana, Edward)
  2. Notice the password column — it shows hashed values like $2b$10$K7L1OJ45..., not "password123"
  3. Click on Post — you should see 15 rows with titles, content, and author IDs
  4. Notice id values are 1, 2, 3... (auto-incremented)
  5. Notice createdAt has timestamps — set automatically

Prisma Studio won't start? It requires a direct database connection (postgres://...), not an accelerated URL. Make sure your DATABASE_URL in .env is 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

↑ Index

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:

  1. Drops all tables in the database (deletes all data)
  2. Re-applies all migrations (recreates the tables)
  3. 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 dev instead

Core Concept: Evolving Your Schema in Real Projects

↑ Index

In a real project, your data model will change over time. You might need to:

  • Add a new field to User (like bio: String?)
  • Add a relationship to a new model (like Comment)
  • Change an existing field type (like email: Stringemail: String with @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:

  1. Edit schema.prisma with your changes
  2. Run:
    npx prisma migrate dev --name add_bio_to_user
    
    Prisma will:
    • Detect what changed
    • Create a migration file
    • Apply it to your local database
    • Regenerate the Prisma Client
  3. 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:

  1. Edit schema.prisma with your changes
  2. Run:
    npx prisma migrate dev --name your_change_name
    
    This creates a migration on your local machine
  3. Review the migration file in prisma/migrations/<timestamp>_your_change_name/migration.sql — ensure it does what you expect
  4. Commit and deploy:
    • Push the changes to Git (including the new migration file)
    • Deploy to staging/production
    • Run on the server:
      npx prisma migrate deploy
      
      This applies pending migrations without re-seeding

migrate deploy vs migrate dev:

CommandWhat it doesWhen to use
migrate devCreates a migration, applies it, regenerates clientLocal development
migrate deployApplies existing migrations without creating new onesStaging/Production
migrate resetDrops all tables, re-applies migrations, re-seedsResetting local dev

Scenario 3: Destructive Changes (Rename or Delete Fields)

You want to rename or delete a field. Data will be lost.

Example: Rename biobiography

  1. Edit schema:
    model User {
      id         String @id @default(cuid())
      biography  String? // ← Renamed from 'bio'
    }
    
  2. Run:
    npx prisma migrate dev --name rename_bio_to_biography
    
  3. 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 deploy in 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

SituationCommandNotes
Add a new field (local dev)npx prisma migrate dev --name add_fieldSafe, can reset after
Add a new field (production)npx prisma migrate dev locally, then npx prisma migrate deploy on serverData preserved
Delete/rename fieldnpx prisma migrate dev --name change_nameReview SQL carefully — data loss!
Forgot to run migrations?npx prisma migrate deployApplies pending migrations
Fresh start (local only)npx prisma migrate resetDeletes everything, re-seeds
Add a related modelnpx prisma migrate dev --name add_model_nameCreates new table + foreign keys
Update seeding logicEdit seed-data.ts or seed.ts, then npx prisma migrate resetRe-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

↑ Index

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:

  1. Detect schema changes
  2. Create a migration
  3. Apply the migration to your local database
  4. 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:

SituationDo you need npx prisma generate?Why
You changed schema.prisma and ran npx prisma migrate devUsually nomigrate dev regenerates the client
You changed schema.prisma but have not run any Prisma command yetYesYour code still uses the old generated client
You ran npx prisma db pullYesThe schema changed from introspection, but your client must be rebuilt
You pulled someone else's schema changes from GitYesYour local generated client may still reflect the old schema
You deleted app/generated/prisma/client or cleaned build artifactsYesThe generated files must be recreated
TypeScript says a new field/model does not existYesYour 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:

  1. Did I run npx prisma migrate dev?
  2. If not, did I at least run npx prisma generate?
  3. 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

↑ Index

ConceptWhat it means
SeedingPopulating a database with initial data using a script
prisma db seedRuns the seed command defined in prisma.config.ts
Password hashing (bcrypt)A one-way transformation that makes stored passwords unreadable
Salt roundsControls 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 adapterConnects Prisma Client to a PostgreSQL database
prisma migrate resetDrops 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.