DEV Community

Cover image for I wanted migration-style seeds for Prisma, so I built them
Angel Gonzalez
Angel Gonzalez

Posted on

I wanted migration-style seeds for Prisma, so I built them

Data that lives in your code

Almost every app I've worked on has data that isn't created by users. It ships with the code:

  • Roles and permissions: admin, editor, viewer.
  • Categories behind a multi-select.
  • Location data: countries and their states or provinces.

Your app depends on these rows existing. They have to be the same in development, staging and production. And they change over time: a new role, a renamed category, a country you didn't support last year.

Prisma handles your schema with migrations. But what about the rows inside it?

The problem with one seed script

Prisma's recommended approach is a single seed script that prisma db seed runs. It looks fine on day one:

// prisma/seed.ts
async function main() {
  await db.role.createMany({ data: roles })
  await db.category.createMany({ data: categories })
  await db.country.createMany({ data: countries })
  await db.state.createMany({ data: states })
  // ...and everything you add next month
}

main()
Enter fullscreen mode Exit fullscreen mode

Then the data starts to change, and the script runs into problems:

  • It runs everything, every time. Running it twice either fails on duplicates or forces you to make every line idempotent.
  • It doesn't know what has already run. There's no record of which data your production database already has.
  • You can't undo one piece. Removing a category you seeded by mistake means writing a one-off script.
  • Changing data is awkward. You either reset the whole database or hand-write updates somewhere else.
  • It only grows. Roles, categories and locations all end up in one file with no history.

It works, but it isn't elegant. And it gets harder to trust the longer the project lives.

I'd already solved this for schemas

I never let my seed script get out of hand. Before Prisma, I had used migration libraries in other projects. They solve exactly this problem for schema changes:

  • Each change lives in its own timestamped file.
  • Each file has an up function to apply it and a down function to revert it.
  • A table records which files have run, so each one runs only once.

So when I started a new project with Prisma, I went looking for the same thing for seeds. I couldn't find a library that did it. So I built one.

Meet prisma-seeder

prisma-seeder brings migration-style seeds to Prisma. Each seed is a file with an up and a down function that receive your Prisma client.

  • Runs each seed once: applied seeds are recorded in a _prisma_seeds table.
  • Reverts seeds with down, latest first or by name.
  • Re-runs seeds after you change their data, with refresh or reset.
  • Generates seed files with a timestamp, so they always run in order.
  • Typed seeds: db knows your models, with autocomplete.
  • Works with Prisma ORM 4 to 8.

I've used it in my own project since the first release.

Seeding roles with Prisma 7

Let's seed a list of roles. The example uses Prisma 7 with PostgreSQL. The README has the setup for Prisma 4, 5, 6 and 8.

1. Install

npm install prisma-seeder
npm install --save-dev tsx
Enter fullscreen mode Exit fullscreen mode

The examples use tsx to run TypeScript. If you use Bun, swap npx tsx for bun in the commands below and in the seed line of prisma.config.ts.

2. Add the table that tracks applied seeds

Add the PrismaSeeds model next to your Role model. @@ignore keeps it out of your Prisma Client.

// prisma/schema.prisma
model Role {
  id   String @id @default(uuid())
  name String @unique
}

model PrismaSeeds {
  name String @id()

  @@map("_prisma_seeds")
  @@ignore
}
Enter fullscreen mode Exit fullscreen mode
npx prisma migrate dev
npx prisma generate
Enter fullscreen mode Exit fullscreen mode

3. Create the seed script

The seed script is a small CLI. The Register declaration gives your seeds your client's type.

// prisma/seed.ts
import { PrismaPg } from '@prisma/adapter-pg'
import path from 'node:path'
import { PrismaSeeder } from 'prisma-seeder'
import { PrismaClient } from '../generated/prisma/client.js'

const db = new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL! }) })

declare module 'prisma-seeder' {
  interface Register {
    client: typeof db
  }
}

const seeder = new PrismaSeeder({
  client: db,
  seeds: path.join(import.meta.dirname, './seeds/*.{ts,js}')
})

await seeder.run()

process.exit()
Enter fullscreen mode Exit fullscreen mode

This script uses top-level await and import.meta.dirname, so your project should be ESM: add "type": "module" to your package.json.

Register it in prisma.config.ts, so prisma db seed applies your pending seeds:

// prisma.config.ts
import 'dotenv/config'
import { defineConfig } from 'prisma/config'

export default defineConfig({
  schema: 'prisma/schema.prisma',
  migrations: {
    path: 'prisma/migrations',
    seed: 'tsx prisma/seed.ts up'
  },
  datasource: {
    url: process.env.DATABASE_URL!
  }
})
Enter fullscreen mode Exit fullscreen mode

4. Write the data with fixed UUIDs

This is the most important part. Generate each id once (for example with crypto.randomUUID() or uuidgen) and write it into the data file. A fixed id means the seed always targets the same rows, so you can run it again safely.

Keep data files out of the folder your glob matches, for example in a data subfolder:

// prisma/seeds/data/roles.ts
export const roles = [
  { id: '0f8b6c1e-3a52-4d7e-9b1a-6c2f4e8d9a01', name: 'admin' },
  { id: '5d2a9e47-81c3-4b6f-a0d8-3e7c1b9f2a64', name: 'editor' },
  { id: 'b93c4f10-6e2d-47a8-8c5b-1f0a7d3e6b29', name: 'viewer' }
]
Enter fullscreen mode Exit fullscreen mode

5. Generate and write the seed

npx tsx prisma/seed.ts generate --name add_roles
Enter fullscreen mode Exit fullscreen mode

This creates prisma/seeds/20261011120000_add_roles.ts. Fill in up and down:

import type { Seed } from 'prisma-seeder'
import { roles } from './data/roles.js'

export const up: Seed = async (db) => {
  // All roles are saved, or none are
  await db.$transaction(
    roles.map((role) =>
      // upsert lets refresh run this seed again with updated data
      db.role.upsert({
        where: { id: role.id },
        create: role,
        update: { name: role.name }
      })
    )
  )
}

export const down: Seed = async (db) => {
  await db.role.deleteMany({
    where: { id: { in: roles.map((role) => role.id) } }
  })
}
Enter fullscreen mode Exit fullscreen mode

6. Apply it

npx prisma db seed
Enter fullscreen mode Exit fullscreen mode

Run it again and nothing happens: the seed is already recorded in _prisma_seeds.

7. Change the data

Say viewer should be called reader. Edit the data file, then re-run only that seed:

npx tsx prisma/seed.ts refresh --name 20261011120000_add_roles
Enter fullscreen mode Exit fullscreen mode

The fixed id finds the existing row, and upsert renames it. No database reset needed. To run down first and then up, use reset instead of refresh.

For brand-new data, such as a new set of categories, generate a new seed. prisma db seed applies only that one.

8. Revert it

# Revert the last applied seed
npx tsx prisma/seed.ts down

# Or a specific one
npx tsx prisma/seed.ts down --name 20261011120000_add_roles
Enter fullscreen mode Exit fullscreen mode

A few more things

Seeds per environment. Roles belong everywhere. Fake users belong only in tests. Put them in separate folders with one seed script each, such as seeds/*.ts and seeds/test/*.ts. Each script only sees and reverts its own seeds. The README walks through it.

Custom templates. The template option sets what generate writes, if your team has its own seed boilerplate.

Limitations, honestly:

  • You add the PrismaSeeds model to your schema yourself. If prisma-seeder created the table on its own, Prisma would see it as drift and offer to reset your database.
  • MongoDB isn't supported.
  • Prisma 8 has no prisma db seed, so there you run seeds with an npm script instead.

Try it

If your seed script handles roles, categories or locations and you're tired of resetting the database to change one row, give it a try:

npm install prisma-seeder
Enter fullscreen mode Exit fullscreen mode

If it helps you, a star on GitHub helps other developers find it. Bug reports, ideas and pull requests are welcome.

How do you handle reference data in your Prisma projects today? Tell me in the comments.

Top comments (1)

Collapse
 
rasinmuhammed profile image
Muhammed Rasin O M •

I split it the same way: reference data (roles, countries) lives in versioned seeds like yours, and anything relational and fake is generated per environment from a fixed random seed so it is reproducible but never committed. The part that usually bites is the fake data pointing at reference rows that a down() just removed. Do you check foreign keys after a revert, or rely on the database to complain?

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more