top of page

Automate Database Seeding with Prisma in 2026

Computer screen displaying code with "Automate Database Seeding" text. Progress at 98%. Dimly lit room with a lamp, keyboard, and cup.

Empty databases kill productivity. You clone a repo, run migrations, and stare at blank tables wondering how to test anything. New developers waste hours creating test data manually. Database resets mean starting over from scratch every time.

Prisma's seeding system fixes this by automating data population for development and testing. Here's how to implement it properly in 2026.

Project Structure and Initial Setup

A standard Prisma project organizes seeding files like this:

project-root/
├── prisma/
│   ├── schema.prisma
│   ├── seed.ts
│   └── migrations/
├── package.json
└── src/

Your seed.ts file lives in the prisma/ directory. Configure package.json to run it:

{
  "prisma": {
    "seed": "tsx prisma/seed.ts"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "tsx": "^4.7.0",
    "typescript": "^5.4.0"
  }
}

Run seeding with npx prisma db seed. Prisma automatically triggers this during npx prisma migrate reset and when creating new development databases.

Build Idempotent Seeds with Upsert

Seed scripts need to run multiple times without breaking. Using create() fails on the second run when records exist. The upsert() method creates new records or updates existing ones:

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

const prisma = new PrismaClient();

async function main() {
  const admin = await prisma.user.upsert({
    where: { email: 'admin@example.com' },
    update: {},
    create: {
      email: 'admin@example.com',
      name: 'Admin User',
      role: 'ADMIN',
    },
  });

  const testUser = await prisma.user.upsert({
    where: { email: 'test@example.com' },
    update: { name: 'Test User Updated' },
    create: {
      email: 'test@example.com',
      name: 'Test User',
      role: 'USER',
      posts: {
        create: [
          {
            title: 'First Post',
            content: 'Testing content',
            published: true,
          },
        ],
      },
    },
  });

  console.log({ admin, testUser });
}

main()
  .then(async () => {
    await prisma.$disconnect();
  })
  .catch(async (error) => {
    console.error(error);
    await prisma.$disconnect();
    process.exit(1);
  });

First run creates everything. Subsequent runs find existing records by email and update them (or skip updates with empty update: {}). The nested posts.create syntax establishes relationships during creation.

Batch Operations for Performance

Individual creates execute separate database queries. Testing 1,000 records shows the difference:

  • Individual creates: 12-15 seconds

  • Batch operations: 1.5-2 seconds

That's an 85% speed improvement. Use createMany() for bulk inserts:

async function seedUsersBatch() {
  const users = Array.from({ length: 1000 }).map((_, i) => ({
    email: `user${i}@example.com`,
    name: `User ${i}`,
    role: 'USER',
  }));

  await prisma.user.createMany({
    data: users,
    skipDuplicates: true,
  });
}

The skipDuplicates: true flag prevents errors when records exist. The database ignores duplicates instead of throwing constraint violations.

Generate Realistic Data with Faker

Manual test data doesn't scale. Faker generates realistic information automatically:

import { PrismaClient } from '@prisma/client';
import { faker } from '@faker-js/faker';

const prisma = new PrismaClient({ log: [] });

async function seedWithFaker() {
  const users = Array.from({ length: 500 }).map(() => ({
    email: faker.internet.email(),
    name: faker.person.fullName(),
    bio: faker.lorem.paragraph(),
    avatar: faker.image.avatarLegacy(),
    createdAt: faker.date.past({ years: 2 }),
  }));

  await prisma.user.createMany({
    data: users,
    skipDuplicates: true,
  });

  const createdUsers = await prisma.user.findMany({
    select: { id: true },
  });

  const posts = Array.from({ length: 2000 }).map(() => ({
    title: faker.lorem.sentence(),
    content: faker.lorem.paragraphs(3),
    published: faker.datatype.boolean(),
    userId: faker.helpers.arrayElement(createdUsers).id,
    createdAt: faker.date.past({ years: 1 }),
  }));

  await prisma.post.createMany({ data: posts });

  console.log(`Seeded ${users.length} users and ${posts.length} posts`);
}

Create parent records first, fetch their IDs, then create child records. This approach works better than nested creates for large datasets.

Import CSV Files and External Data

Production-like data often comes from CSV exports. Import it using Papaparse:

import { PrismaClient } from '@prisma/client';
import * as fs from 'fs';
import Papa from 'papaparse';

const prisma = new PrismaClient();

async function seedFromCSV() {
  const csvFile = fs.readFileSync('./data/users.csv', 'utf8');
  const parsed = Papa.parse(csvFile, {
    header: true,
    skipEmptyLines: true,
    dynamicTyping: true,
  });

  const users = parsed.data.map((row: any) => ({
    email: row.email?.trim(),
    name: row.name?.trim(),
    role: row.role || 'USER',
  })).filter(user => user.email && user.name);

  await prisma.user.createMany({
    data: users,
    skipDuplicates: true,
  });

  console.log(`Imported ${users.length} users from CSV`);
}

Teams working on mobile app development in Maryland often need realistic datasets matching production scale. CSV imports provide actual data distributions for load testing without exposing customer information.

Environment-Specific Seeding

Different environments need different data volumes. Production needs minimal seeds (admin users, settings). Development needs large realistic datasets. Tests need predictable minimal data:

import { parseArgs } from 'node:util';

async function main() {
  const { values } = parseArgs({
    options: {
      environment: { type: 'string' },
      count: { type: 'string' },
    },
  });

  const env = values.environment || 'development';
  const userCount = values.count ? parseInt(values.count) : 50;

  switch (env) {
    case 'production':
      await seedProduction();
      break;
    case 'test':
      await seedTest();
      break;
    default:
      await seedDevelopment(userCount);
  }
}

Run with environment flags:

npx prisma db seed -- --environment production
npx prisma db seed -- --environment development --count 1000

Docker and CI/CD Integration

Add seeding to Docker Compose:

version: '3.8'
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp

  app:
    build: .
    depends_on:
      - postgres
    command: >
      sh -c "npx prisma migrate deploy &&
             npx prisma db seed &&
             npm start"

GitHub Actions workflow:

name: Test with Seeded Database
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: password

    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      
      - run: npm ci
      - run: npx prisma migrate deploy
      - run: npx prisma db seed -- --environment test
      - run: npm test

Common Issues and Solutions

"Cannot find module './prisma/seed'" Check package.json has the correct seed command. Verify prisma/seed.ts exists (not .js if using TypeScript).

Seeding hangs indefinitely Missing await prisma.$disconnect(). Always disconnect:

main()
  .then(async () => await prisma.$disconnect())
  .catch(async (e) => {
    console.error(e);
    await prisma.$disconnect();
    process.exit(1);
  });

Foreign key constraint failures Create parent records before children. Fetch parent IDs, then create children referencing those IDs.

Out of memory with large datasets Process in batches. Create 1,000 records at a time instead of 100,000 at once.

Quick Implementation Checklist

  • Configure package.json with seed command

  • Create prisma/seed.ts with Prisma Client

  • Use upsert() for idempotent operations

  • Install @faker-js/faker for test data

  • Use createMany() for batches over 100 records

  • Disable logging: new PrismaClient({ log: [] })

  • Create parents first, then children with their IDs

  • Add environment-specific logic

  • Always call prisma.$disconnect()

  • Test seeds run multiple times successfully

Proper seed scripts save weeks of cumulative development time. New developers onboard in minutes. Database resets complete instantly. Testing with realistic data becomes standard practice. The hour spent building seeds pays back immediately.

Comments


bottom of page