Skip to main content

Next.js Database Seeding

Introduction

Database seeding is the process of populating a database with initial data. This is particularly useful during development, testing, or when deploying a new application that requires some default data to function properly. In Next.js applications, database seeding helps ensure that your application has consistent data across different environments and can be especially valuable for:

  • Development environments, where developers need realistic data to work with
  • Testing environments, where tests require predictable data sets
  • Production environments, where certain default data (like user roles, categories, or settings) must exist for the application to function

In this guide, we'll explore how to implement database seeding in Next.js applications using different database technologies.

Understanding Database Seeding

What is Database Seeding?

Database seeding is a controlled process to add initial records to your database. These records might include:

  • Default user accounts
  • Configuration settings
  • Reference data (like countries, categories, or product types)
  • Sample content for development and demonstration purposes

Why Seed Your Database?

Seeding your database offers several advantages:

  • Consistency: Ensures all environments have the same baseline data
  • Development Efficiency: Developers don't need to manually create test data
  • Testing: Provides known data states for reliable testing
  • Demonstration: Creates sample data to showcase application features

Basic Seeding Approaches in Next.js

Next.js offers flexibility in how you approach database seeding. Let's explore several methods:

1. Using Scripts

The simplest approach is to create dedicated Node.js scripts that you can run separately from your application.

Example with Prisma

First, create a seed script in your project:

typescript
// prisma/seed.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
// Create users
const alice = await prisma.user.upsert({
where: { email: '[email protected]' },
update: {},
create: {
email: '[email protected]',
name: 'Alice',
posts: {
create: [
{
title: 'Getting Started with Next.js',
content: 'This is my first post!',
published: true,
},
],
},
},
})

const bob = await prisma.user.upsert({
where: { email: '[email protected]' },
update: {},
create: {
email: '[email protected]',
name: 'Bob',
posts: {
create: [
{
title: 'Understanding Database Seeding',
content: 'Here is my take on database seeding...',
published: true,
},
],
},
},
})

console.log({ alice, bob })
}

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

Then configure your package.json to use this seed script:

json
{
"scripts": {
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts"
}
}

Run the script with:

bash
npm run seed

2. Seeding During Development with API Routes

You can create a special API route that triggers seeding when called:

typescript
// pages/api/seed.ts (Next.js Pages Router)
import { PrismaClient } from '@prisma/client'
import type { NextApiRequest, NextApiResponse } from 'next'

const prisma = new PrismaClient()

// Only allow this route in development
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
if (process.env.NODE_ENV === 'production') {
return res.status(403).json({ message: 'Forbidden in production' })
}

try {
// Your seeding logic here
await prisma.category.createMany({
data: [
{ name: 'Electronics' },
{ name: 'Books' },
{ name: 'Clothing' },
],
skipDuplicates: true,
})

return res.status(200).json({ message: 'Database seeded successfully' })
} catch (error) {
console.error(error)
return res.status(500).json({ message: 'Failed to seed database', error })
} finally {
await prisma.$disconnect()
}
}

Or with App Router:

typescript
// app/api/seed/route.ts (Next.js App Router)
import { PrismaClient } from '@prisma/client'
import { NextResponse } from 'next/server'

const prisma = new PrismaClient()

export async function GET() {
// Only allow this route in development
if (process.env.NODE_ENV === 'production') {
return NextResponse.json(
{ message: 'Forbidden in production' },
{ status: 403 }
)
}

try {
// Your seeding logic
await prisma.category.createMany({
data: [
{ name: 'Electronics' },
{ name: 'Books' },
{ name: 'Clothing' },
],
skipDuplicates: true,
})

return NextResponse.json({ message: 'Database seeded successfully' })
} catch (error) {
console.error(error)
return NextResponse.json(
{ message: 'Failed to seed database', error },
{ status: 500 }
)
} finally {
await prisma.$disconnect()
}
}

3. Seeding During Application Initialization

For some use cases, you might want to seed essential data when your application first starts:

typescript
// lib/seed.ts
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

export async function seedEssentialData() {
// Check if essential data already exists
const settingsCount = await prisma.settings.count()

if (settingsCount === 0) {
// Seed essential settings
await prisma.settings.create({
data: {
siteName: 'My Next.js App',
maintenanceMode: false,
theme: 'light',
},
})
console.log('Essential settings seeded!')
}

await prisma.$disconnect()
}

Then call this in a server component or during app initialization:

typescript
// app/layout.tsx or another appropriate place
import { seedEssentialData } from '@/lib/seed'

// Call this once during app initialization
// Be careful with this approach as it runs on every app startup
if (process.env.NODE_ENV === 'production') {
seedEssentialData()
.catch(console.error)
}

Advanced Seeding Techniques

Working with Multiple Environments

For more complex applications, you might need different seed data for different environments:

typescript
// lib/seed/index.ts
import { seedDevelopment } from './development'
import { seedTest } from './test'
import { seedProduction } from './production'

export async function seedDatabase() {
const environment = process.env.NODE_ENV || 'development'

switch (environment) {
case 'development':
await seedDevelopment()
break
case 'test':
await seedTest()
break
case 'production':
await seedProduction()
break
default:
throw new Error(`Unsupported environment: ${environment}`)
}
}

Using Faker.js for Realistic Test Data

For development environments, you might want more realistic but randomized data:

typescript
// lib/seed/development.ts
import { PrismaClient } from '@prisma/client'
import { faker } from '@faker-js/faker'

const prisma = new PrismaClient()

export async function seedDevelopment() {
// Create 50 random products
const categories = ['Electronics', 'Books', 'Clothing', 'Home', 'Sports']

// First create categories
for (const categoryName of categories) {
await prisma.category.upsert({
where: { name: categoryName },
update: {},
create: { name: categoryName },
})
}

// Then create products
for (let i = 0; i < 50; i++) {
const categoryName = faker.helpers.arrayElement(categories)
const category = await prisma.category.findUnique({
where: { name: categoryName },
})

await prisma.product.create({
data: {
name: faker.commerce.productName(),
description: faker.commerce.productDescription(),
price: parseFloat(faker.commerce.price()),
stock: faker.number.int({ min: 0, max: 100 }),
categoryId: category?.id || 1,
},
})
}

console.log('Development database seeded with 50 fake products')
}

Database-Specific Seeding Examples

MongoDB with Mongoose

For MongoDB users, here's how to seed data using Mongoose:

typescript
// lib/seed-mongoose.ts
import mongoose from 'mongoose'
import User from '@/models/User'
import Product from '@/models/Product'

export async function seedMongoDatabase() {
// Connect to MongoDB if not already connected
if (mongoose.connection.readyState !== 1) {
await mongoose.connect(process.env.MONGODB_URI as string)
}

// Check if data already exists
const userCount = await User.countDocuments()

if (userCount === 0) {
// Seed admin user
await User.create({
name: 'Admin User',
email: '[email protected]',
password: 'hashed_password_here', // In reality, use proper password hashing
role: 'admin',
})

// Seed products
await Product.insertMany([
{
name: 'Laptop',
price: 1299.99,
description: 'A powerful laptop for developers',
},
{
name: 'Smartphone',
price: 699.99,
description: 'The latest smartphone model',
},
])

console.log('MongoDB seeded successfully!')
} else {
console.log('Skipping seed - database already has data')
}
}

SQL with Sequelize

If you're using Sequelize with a SQL database:

typescript
// lib/seed-sequelize.ts
import { sequelize } from '@/lib/db'
import { User, Product } from '@/models'

export async function seedSequelizeDatabase() {
// Sync models with database
await sequelize.sync()

// Check if data already exists
const userCount = await User.count()

if (userCount === 0) {
// Seed users
await User.bulkCreate([
{
name: 'John Doe',
email: '[email protected]',
password: 'hashed_password_here', // Use proper hashing in real code
},
{
name: 'Jane Smith',
email: '[email protected]',
password: 'hashed_password_here',
},
])

// Seed products
await Product.bulkCreate([
{
name: 'Ergonomic Chair',
price: 299.99,
description: 'A comfortable chair for long coding sessions',
},
{
name: 'Monitor',
price: 499.99,
description: '4K monitor with excellent color accuracy',
},
])

console.log('SQL database seeded successfully!')
}
}

Best Practices for Database Seeding

To ensure your seeding process is reliable and maintainable, follow these best practices:

  1. Idempotence: Make your seed scripts idempotent, meaning they can run multiple times without creating duplicate data.

  2. Environment Awareness: Create different seeding strategies for different environments (development, test, production).

  3. Version Control: Keep your seed data in version control alongside your application code.

  4. Separation of Concerns: Split large seeding scripts into smaller, focused modules.

  5. Error Handling: Implement proper error handling and logging in your seed scripts.

  6. Data Dependencies: Handle relationships and dependencies between data entities correctly.

  7. Use Transactions: When appropriate, wrap seeding operations in database transactions to ensure atomicity.

typescript
// Example of using transactions with Prisma
async function seedWithTransaction() {
try {
await prisma.$transaction(async (tx) => {
// All operations here are part of the same transaction
await tx.user.create({ data: { name: 'User 1' } })
await tx.product.createMany({
data: [
{ name: 'Product 1', price: 10.99 },
{ name: 'Product 2', price: 15.99 },
],
})
})
console.log('Seed transaction completed successfully')
} catch (error) {
console.error('Seed transaction failed:', error)
// The entire transaction is rolled back if any operation fails
}
}

Integration with Next.js Deployment Workflows

Seeding in CI/CD Pipelines

For more advanced applications, you might want to integrate seeding into your CI/CD pipeline:

yaml
# Example GitHub Actions workflow snippet
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run database migrations
run: npx prisma migrate deploy
- name: Seed database
run: npm run seed
- name: Deploy Next.js app
run: npm run deploy

Seeding in Docker Environments

If you're using Docker, you can incorporate seeding into your startup process:

dockerfile
# Dockerfile
FROM node:18-alpine AS base

# ... other Docker setup ...

# Copy seed script
COPY prisma/seed.ts ./prisma/

# Define command that includes migration and seeding
CMD npx prisma migrate deploy && npm run seed && npm start

Summary

Database seeding is a crucial part of managing your Next.js application's data lifecycle. Whether you're developing locally, running tests, or deploying to production, having a reliable and automated way to populate your database with initial data improves development productivity and application stability.

In this guide, we've covered:

  • Basic concepts of database seeding
  • Different approaches to implementing seeding in Next.js
  • Database-specific examples for popular technologies
  • Best practices for maintaining seed scripts
  • Integration with deployment workflows

By implementing proper database seeding strategies, you can ensure your Next.js applications start with consistent, reliable data across all environments.

Additional Resources

Exercises

  1. Create a simple Next.js app with Prisma and implement a seed script that adds at least three different types of related entities.

  2. Implement environment-specific seeding that creates minimal data for production but comprehensive test data for development.

  3. Build a seed management UI in your Next.js app that allows administrators to reset specific portions of the database to their initial state.

  4. Create a seeding strategy that incorporates external data from a CSV or JSON file.

  5. Implement a system that tracks which seed versions have been run, similar to how database migrations work.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)