A seed script has three jobs: refuse to run against the wrong database, be repeatable, and finish fast. The first is the one people skip — check the database name before the first write, because a copied MONGO_URI is the usual way production data disappears. Repeatability comes from fixed _id values: give every seed document a literal ObjectId in seed/data.js, upsert instead of dropping collections, and a rerun leaves the ids in your open URLs valid.
const dbName = new URL(uri.replace('mongodb', 'http')).pathname.slice(1);
if (process.env.NODE_ENV === 'production' || !/_dev$|_test$/.test(dbName)) {
console.error(`refusing to seed "${dbName}": name must end in _dev or _test`);
process.exit(1);
}
const upsert = (doc) =>
({ updateOne: { filter: { _id: doc._id }, update: { $set: doc }, upsert: true } });
await Review.deleteMany({}); // derived data: rebuild it
await Author.bulkWrite(authors.map(upsert));
await Book.bulkWrite(books.map(upsert));
await User.bulkWrite(users.map(({ password, ...u }) =>
upsert({ ...u, passwordHash: bcrypt.hashSync(password, 10) })));
for (const M of [Author, Book, Review, User]) await M.syncIndexes();$ MONGO_URI=mongodb://127.0.0.1:28113/bookshelf_dev node seed/seed.js seeded bookshelf_dev in 274 ms authors: 3 books: 5 reviews: 0 users: 2
A second run reports 190 ms and identical counts; pointing it at bookshelf exits 1. syncIndexes() belongs here rather than in the application: it creates the indexes the schemas declare and drops the ones they do not, which is wrong for production and right for a developer machine (Mongoose Transactions). Hash the fixture passwords at seed time — a bcrypt hash in source control is a user nobody can log in as.