Skip to content

Seeding

Demo data lives in the seed crate — a product crate under crates/, beside migrations. Seeding is idempotent: every factory inserts with ON CONFLICT DO NOTHING, so nestrs run db seed can run any number of times and only ever fills gaps. nestrs run db reset chains a fresh migrate with a seed for a clean slate.

One factory per table, each a seed(db) function that inserts its demo rows and returns how many landed. Organizations use fixed UUIDs so other factories — and tests — can reference them by constant:

crates/seed/src/factories/org.rs
pub const ACME: Uuid = Uuid::from_u128(0x0000_0000_0000_0000_0000_0000_0000_ac3e);
pub const GLOBEX: Uuid = Uuid::from_u128(0x0000_0000_0000_0000_0000_0000_0000_61b3);
const DEMO_ORGS: [(Uuid, &str); 2] = [(ACME, "Acme"), (GLOBEX, "Globex")];
pub async fn seed(db: &DatabaseConnection) -> Result<u64> {
let mut inserted = 0;
for (id, name) in DEMO_ORGS {
let stmt = Query::insert()
.into_table(Org::Table)
.columns([Org::Id, Org::Name])
.values_panic([id.into(), name.to_owned().into()])
.on_conflict(OnConflict::column(Org::Id).do_nothing().to_owned())
.to_owned();
inserted += db.execute(&stmt).await?.rows_affected();
}
Ok(inserted)
}

Rows that depend on others reach for those constants. Demo users also use fixed ids — user::ACME_AUTHOR is the author of Acme posts — and conflict on the unique email:

crates/seed/src/factories/user.rs
pub const ACME_AUTHOR: Uuid = Uuid::from_u128(0x0000_0000_0000_0000_0000_0000_0000_ac01);
const DEMO: [(Uuid, Uuid, &str, &str); 5] = [
(ACME_AUTHOR, org::ACME, "Acme Author", "acme-user-1@example.test"),
// ...
];
// INSERT ... ON CONFLICT (email) DO NOTHING.

Posts hang off the org and author constants. post::WELCOME is a stable id for the first Acme article:

crates/seed/src/factories/post.rs
pub const WELCOME: Uuid = Uuid::from_u128(0x0000_0000_0000_0000_0000_0000_0000_b001);
const DEMO: [(Uuid, Uuid, Uuid, &str, &str); 3] = [
(
WELCOME,
org::ACME,
user::ACME_AUTHOR,
"Welcome to Publish",
"Getting started with nestrs and the Publish demo app.",
),
// ...
];
// INSERT ... ON CONFLICT (id) DO NOTHING.

seed::run calls each factory in dependency order and sums the inserts — parents before children, so foreign keys resolve:

crates/seed/src/runner.rs
pub async fn run(db: &DatabaseConnection) -> Result<u64> {
let mut inserted = 0;
inserted += factories::org::seed(db).await?;
inserted += factories::user::seed(db).await?;
inserted += factories::post::seed(db).await?;
Ok(inserted)
}
Terminal window
nestrs run db seed # load demo data (idempotent)
nestrs run db reset # fresh migrate, then seed

nestrs run db seed runs the crate’s seed binary, which reads NESTRS_DATABASE__URL, runs seed::run, and prints how many rows it inserted.

Because run is a plain function, tests call it directly. The crate’s e2e migrates a throwaway database, seeds it, asserts rows landed, then seeds again and asserts the second pass inserts zero — idempotence, verified.

  • Migrations — the schema the seed data fills.
  • Database — entities and the service that reads them back.