Automate Database Seeding with Prisma in 2026
- Devin Rosario
- Nov 20
- 4 min read

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