How to Seed a Database With Test Data for Local Development
New hire clones the repo, runs the setup docs, and hits an app with zero rows in it. Every list view is empty, every relationship is untestable, and the first hour of actual onboarding is spent hand-typing a customer and an order just to see the UI do something. The fix engineering teams converge on is a seed script — a repeatable command that fills a fresh local database with data that looks like production without being production. Here's how to seed a database with test data properly: realistic volumes, valid foreign keys, and a script anyone on the team can re-run without thinking about it.
To seed a database with test data means running a script — usually
wired up as npm run seed or similar — that inserts a batch of
synthetic rows into an empty or freshly reset local database, in an
order that respects foreign key constraints, so the application has
believable data to render against from the first npm run dev. It's
distinct from a database migration, which changes schema, not rows, and
distinct from copying a production snapshot, which brings real
customer data onto every developer's laptop.
Why not just copy production data?
Cloning a production dump onto a laptop is the fastest way to get realistic-looking data, and it's also the fastest way to put real customer PII — names, emails, addresses — on every machine that clones the repo, with none of the access controls production has. It's also usually stale within days, and it's frequently the wrong shape: a feature you're building might not have any real rows yet, so there's nothing to copy. Synthetic data sidesteps both problems. It's generated fresh against your actual schema, so it's never out of date, and it never touches a real customer's data in the first place.
How do you seed a database with realistic test data?
The mechanics of a seed script are the same regardless of what
generates the rows: connect to the local database, and for each table,
insert a batch of records shaped like the table's columns, with foreign
keys pointing at rows that already exist. The part that's easy to get
wrong by hand is realism — a created_at that isn't actually in the
past, a full_name that's just Test User 1, Test User 2, an
email that never varies in format. None of that breaks the app, but
all of it makes the dev environment a worse stand-in for production
than it needs to be, which is exactly when a UI bug (a name too long
for its container, a date formatted wrong) slips past local testing.
A schema- or template-driven generator closes that gap. Instead of
writing getRandomFullName()-style calls by hand for every column,
you describe the shape of a row once — or generate straight from a
template — and the engine fills in realistic values: real-looking
names via getRandomFullName,
addresses via getRandomAddress,
dates within a real range via
getRandomDate, and formatted numbers
via formatNumber. A single call to
JsonFabrica's batch generation API can
return hundreds of rows shaped exactly like a table, ready to insert:
curl -s -X POST https://api.jsonfabrica.com/v1/batches \
-H 'Authorization: Bearer sk_...' \
-H 'content-type: application/json' \
-d '{
"documents": [
{ "templateId": "tpl_customer", "alias": "customer", "count": 200 }
]
}'
Wire that call into a scripts/seed.ts (or .js) file, hook it up to
npm run seed in package.json, and every developer on the team gets
the same populated dev database with one command.
How do you seed tables in the right order?
A child row can't reference a parent that doesn't exist yet, so a seed
script has to insert tables in dependency order: accounts or
organizations first, then users, then the domain entities that belong
to them, like customers, then the records that depend on those, like
orders and line items. Hard-coding this by hand across a handful of
tables is manageable; get past five or six related tables and it's easy
to seed an orders table before the customers it's supposed to
reference, or to end up with an order.customerId that's just a random
number instead of a real customer's id.
JsonFabrica's batch endpoint handles this by letting one document in
the batch reference another document generated earlier in the same
call, through a relations map with a round-robin
strategy — so an order document's customerId field is wired
directly to a customer document's id from the same batch, instead
of your seed script stitching IDs together after the fact:
{
"documents": [
{ "templateId": "tpl_customer", "alias": "customer", "count": 200 },
{
"templateId": "tpl_order",
"alias": "order",
"count": 800,
"relations": { "customerId": { "from": "customer.id", "strategy": "round-robin" } }
}
]
}
List parent documents before the children that relate to them in the
documents array, same as you'd order INSERT statements by hand, and
every generated order lands with a customerId that points at a
customer that was actually created in that same run.
How many rows should you seed for local development?
More rows than "one of each," fewer than a load test. The point of a dev seed isn't volume for its own sake, it's exercising the code paths that only show up once there's something to page through, sort, or filter — an empty state looks fine with zero rows and one row alike, but a pagination bug, an N+1 query, or a sort that silently only handles ascending order won't surface until there are a few hundred rows to work with. A few hundred to a few thousand rows per core table is typically enough to catch that class of bug on a laptop without making the seed script itself slow. Save five- and six-figure row counts — where you actually need to see how the app behaves under real volume — for a dedicated load or performance test, not the everyday local dev database.
How often should you refresh your dev database?
Reset it whenever the schema changes underneath it, whenever the data
in it has drifted from manual testing (a customer you renamed by hand
in the UI, an order you clicked through six different statuses), and
as a routine step in onboarding a new machine. Because a seed script is
just a repeatable command against a freshly migrated, empty database,
"refresh" is nothing more than drop the database, run migrations, run
npm run seed again — not a manual re-entry chore, and not something
that should feel risky to run. If refreshing your dev database feels
like a chore, that's usually a sign the seed script isn't actually
idempotent yet, and it's worth fixing that before it costs the next new
hire their first morning.
FAQ
What does it mean to seed a database with test data? It means populating an empty local database with realistic synthetic records so the application has something to run against, instead of starting from zero rows or a copy of production. A good seed script inserts parent tables before child tables, keeps foreign keys valid, produces enough rows to make the app feel populated, and can be re-run on a fresh database at any time.
Should I seed my dev database with real production data? No. Copying production data into a local environment spreads real customer PII to every laptop and CI runner, and it's rarely current or shaped the way the feature you're building actually needs. Synthetic data generated against your schema gives you realistic volumes and working relationships without any of that exposure.
In what order should I seed database tables? Seed parent tables before the child tables that reference them with a foreign key, since a child row can't point at a customer or order that doesn't exist yet. A typical order is accounts or organizations, then users, then domain entities like customers, then dependent records like orders and order line items.
How many rows should a local dev seed have? Enough to exercise pagination, sorting, and empty-state edge cases, but not so much that the seed script is slow to run. A few hundred to a few thousand rows per core table is usually the sweet spot for a laptop-sized dev database — save five- and six-figure volumes for load or performance testing.
How often should I refresh or reset my local dev database? Refresh it whenever the schema changes, whenever seed data starts to look stale from manual testing, and as a routine part of onboarding a new machine or new hire. Because a good seed script is idempotent, resetting is just dropping the database and re-running it, not a manual data-entry chore.
What's the difference between database seeding and database migrations? Migrations change the shape of the database, such as adding a column or table, and are meant to run against production. Seeding inserts rows of sample data and is meant for local and staging environments only. A seed script typically runs after migrations have brought the schema up to date.
A seed script is only as good as the data it inserts, and hand-rolled
Test User 1 rows only get a dev environment so far. JsonFabrica's
batch generation API generates
schema-shaped, relationally-consistent rows in one call, so npm run seed can produce a dev database that actually looks like production.
Generate realistic test data with JsonFabrica
Describe the shape of your data once, then generate as many fresh, realistic JSON documents as you need via a simple API call.