Write a Seed Script
Write a Seed Script
This script runs outside of Next.js entirely — it’s a plain Node script
you’ll invoke with npm run db:seed, the same way db/init.js applies
your schema.
Add the npm script
Open package.json and add a db:seed entry alongside db:init:
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"db:init": "node db/init.js",
"db:seed": "node db/seed.js"
}
Write the script
Create db/seed.js:
require("dotenv").config({ path: ".env.local" });
const { Pool } = require("pg");
const connectionString = process.env.DATABASE_URL;
if (!connectionString) {
console.error("DATABASE_URL is not set. Copy .env.example to .env.local and fill it in first.");
process.exit(1);
}
const keepExisting = process.argv.includes("--keep");
const GROUPS = ["Work", "Family", "Friends", "Book Club"];
const CONTACTS = [
{ name: "Ada Lovelace", phone: "555-0101", email: "ada@example.com", company: "Analytical Engines Ltd", notes: "Met at the algorithms meetup.", favorite: true, groups: ["Work", "Book Club"] },
{ name: "Grace Hopper", phone: "555-0102", email: "grace@example.com", company: "Navy Research", notes: "", favorite: true, groups: ["Work"] },
{ name: "Alan Turing", phone: "555-0103", email: "alan@example.com", company: "", notes: "", favorite: false, groups: ["Work", "Book Club"] },
{ name: "Priya Ramaswamy", phone: "555-0203", email: "priya.r@example.com", company: "Design Co-op", notes: "Runs the book club on Thursdays.", favorite: true, groups: ["Friends", "Book Club"] },
{ name: "Mom", phone: "555-0301", email: "mom@example.com", company: "", notes: "Call on Sundays.", favorite: true, groups: ["Family"] },
// Add as many more as you like — this is just sample data.
];
async function main() {
const pool = new Pool({ connectionString });
const client = await pool.connect();
try {
await client.query("BEGIN");
if (!keepExisting) {
console.log("Clearing existing contacts and groups...");
await client.query("DELETE FROM contact_groups");
await client.query("DELETE FROM contacts");
await client.query("DELETE FROM groups");
}
const groupIdByName = {};
for (const name of GROUPS) {
const result = await client.query(
`INSERT INTO groups (name) VALUES ($1)
ON CONFLICT (name) DO UPDATE SET name = EXCLUDED.name
RETURNING id`,
[name]
);
groupIdByName[name] = result.rows[0].id;
}
console.log(`Seeded ${GROUPS.length} groups.`);
let count = 0;
for (const c of CONTACTS) {
const result = await client.query(
`INSERT INTO contacts (name, phone, email, company, notes, favorite)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`,
[c.name, c.phone || null, c.email || null, c.company || null, c.notes || null, !!c.favorite]
);
const contactId = result.rows[0].id;
for (const groupName of c.groups) {
const groupId = groupIdByName[groupName];
if (!groupId) continue;
await client.query(
`INSERT INTO contact_groups (contact_id, group_id) VALUES ($1, $2)
ON CONFLICT DO NOTHING`,
[contactId, groupId]
);
}
count++;
}
console.log(`Seeded ${count} contacts.`);
await client.query("COMMIT");
console.log("Done.");
} catch (err) {
await client.query("ROLLBACK");
console.error("Seeding failed:", err.message);
process.exitCode = 1;
} finally {
client.release();
await pool.end();
}
}
main();
A few design decisions worth noticing:
- It clears before it seeds, by default. Without that, running the
script twice would double every contact. Clearing first makes the
script safe to re-run any time you want to reset to a known state —
which is exactly what a seed script is for. --keepis an escape hatch.process.argv.includes("--keep")
checks for a command-line flag; runningnpm run db:seed -- --keep
skips the clearing step, so you can layer sample data onto contacts
you’ve already created by hand.- The whole thing is one transaction. If seeding fails partway
through — a typo in a group name, say —ROLLBACKmeans you’re left
with your previous data intact, not a half-seeded database. - It reuses the exact same group-linking pattern (
ON CONFLICT
upsert for groups, then insert intocontact_groups) you already wrote
by hand in Module 8’ssetContactGroups. Seeing the same pattern show
up in a second, independent piece of code is a good sign it was the
right abstraction the first time.
Test it
npm run db:seed
You should see Seeded 4 groups. and Seeded 5 contacts. in your
terminal. Reload the Home page — your hand-inserted test contacts are gone,
replaced by the seeded set, correctly grouped and favorited. Run it again:
the counts should stay exactly the same, confirming it’s safe to re-run.
Then try:
npm run db:seed -- --keep
You should end up with 10 contacts instead of 5 — the seed data added on
top of what was already there.
Checkpoint
npm run db:seed reliably populates a realistic dataset with contacts and
groups, is safe to run repeatedly, and supports both “start fresh” and
“add on top” modes.