Creating and persisting documents is the write side of every MERN application. Every time a user registers, an author publishes a post, or a reader leaves a comment, your Express controller creates a Mongoose document and saves it to MongoDB. Mongoose gives you three distinct ways to do this โ each with different trade-offs around validation, hooks, and performance. Understanding when each approach is appropriate, how Mongoose applies defaults and validation before saving, and what errors each method can throw turns a source of subtle bugs into a predictable, reliable operation.
Three Ways to Create Documents
| Method | Validates? | Runs Hooks? | Best For |
|---|---|---|---|
Model.create(data) |
Yes | Yes (save hooks) | Standard single-document creation in controllers |
new Model(data).save() |
Yes | Yes (save hooks) | When you need to modify the document before saving |
Model.insertMany(docs) |
Schema validation only | No save hooks | Bulk inserts, database seeding, data imports |
Model.create(data) is shorthand for new Model(data).save(). Both call the same underlying save operation, run all validators, apply all defaults, and trigger pre and post save hooks. Use whichever reads more clearly in context โ create() is more concise; new Model().save() is more explicit when you need to inspect or modify the document before persisting.published: { type: Boolean, default: false }, a document created without a published field will automatically have published: false in the saved document. Check the returned document after create() to see the final state including all applied defaults.req.body directly to Model.create(). This is a mass assignment vulnerability โ a malicious user can send extra fields like { role: 'admin', isEmailVerified: true } that bypass your intended logic. Always destructure only the fields you expect from req.body before passing to create().Model.create() โ Standard Creation
// โโ Creating a single document โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const createPost = asyncHandler(async (req, res) => {
// Destructure only expected fields โ never spread req.body directly
const { title, body, excerpt, tags, published, coverImage } = req.body;
const post = await Post.create({
title,
body,
excerpt,
tags,
published,
coverImage,
author: req.user.id, // from JWT middleware โ never from req.body
});
// post is the saved document including:
// - _id (auto-generated ObjectId)
// - createdAt and updatedAt (from timestamps: true)
// - slug (set by pre('save') hook)
// - published: false (default, if not provided)
// - viewCount: 0 (default)
res.status(201).json({ success: true, data: post });
});
// โโ Creating a user with hashed password โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const register = asyncHandler(async (req, res) => {
const { name, email, password } = req.body;
const user = await User.create({ name, email, password });
// The pre('save') hook in User schema hashes the password automatically
// The saved document has password: '$2b$12$...' (hashed)
// Generate a JWT and return โ do NOT include the password in the response
const token = generateToken(user._id);
res.status(201).json({
success: true,
token,
data: { _id: user._id, name: user.name, email: user.email, role: user.role },
});
});
new Model().save() โ Construct Then Persist
// Use when you need to build the document in stages before saving
const createPost = asyncHandler(async (req, res) => {
const { title, body, tags } = req.body;
// Build the document
const post = new Post({ title, body, tags, author: req.user.id });
// Inspect or modify before saving
if (post.tags.length === 0) {
post.tags = extractTagsFromBody(post.body); // auto-tag from content
}
// Optional: validate without saving (throws if invalid)
await post.validate();
console.log('Validation passed, slug will be:', post.slug); // pre-save hook runs slug
// Save to MongoDB โ runs all validators and pre/post save hooks
await post.save();
res.status(201).json({ success: true, data: post });
});
// Another use case: create then modify in the same flow
const user = new User({ name, email, password });
user.emailVerifyToken = crypto.randomBytes(32).toString('hex');
user.emailVerifyExpires = new Date(Date.now() + 24 * 60 * 60 * 1000); // 24h
await user.save(); // token fields set before saving โ
Model.insertMany() โ Bulk Creation
// Fast bulk insert โ use for seeding or data imports
// Does NOT run save hooks โ password hashing, slug generation etc. will NOT run
// Does run schema validation (required, min, max, enum)
const seedPosts = async (authorId) => {
const posts = [
{ title: 'Post One', body: 'Content 1...'.repeat(5), author: authorId,
slug: 'post-one', published: true },
{ title: 'Post Two', body: 'Content 2...'.repeat(5), author: authorId,
slug: 'post-two', published: true },
{ title: 'Post Three', body: 'Content 3...'.repeat(5), author: authorId,
slug: 'post-three', published: false },
];
const result = await Post.insertMany(posts, { ordered: false });
console.log(`Inserted ${result.length} posts`);
};
// Handling partial failures with ordered: false
try {
const results = await Post.insertMany(docs, { ordered: false });
} catch (err) {
// With ordered: false, err.insertedDocs contains successfully inserted docs
// err.writeErrors contains the errors for failed docs
console.log(`Inserted: ${err.insertedDocs.length}`);
console.log(`Failed: ${err.writeErrors.length}`);
}
What Happens During save()
Document.save() execution order:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
1. pre('validate') hooks run
2. Schema validators run (required, min, max, enum, custom...)
โ ValidationError thrown if any fail โ save() stops here
3. post('validate') hooks run
4. pre('save') hooks run (password hashing, slug generation, etc.)
5. Document is sent to MongoDB
โ MongoServerError thrown for unique constraint violations
6. post('save') hooks run (send welcome email, invalidate cache, etc.)
7. save() resolves with the saved document
Common Mistakes
Mistake 1 โ Passing req.body directly to create()
โ Wrong โ mass assignment allows client to set any field:
const post = await Post.create(req.body);
// User sends { title: '...', body: '...', author: 'differentUserId', featured: true }
// All fields including injected ones are saved to MongoDB
โ Correct โ whitelist only the fields you intend to accept:
const { title, body, tags, excerpt } = req.body;
const post = await Post.create({ title, body, tags, excerpt, author: req.user.id }); // โ
Mistake 2 โ Using insertMany for user registration (skips password hashing hook)
โ Wrong โ inserting users in bulk with insertMany skips the pre(‘save’) hook that hashes passwords:
await User.insertMany([
{ name: 'Alice', email: 'alice@example.com', password: 'plaintext123' },
]); // password stored as plaintext โ security breach!
โ Correct โ for users, always use create() or save() so the hashing hook runs:
await User.create({ name: 'Alice', email: 'alice@example.com', password: 'plaintext123' });
// pre('save') hook hashes the password before it reaches MongoDB โ
Mistake 3 โ Not handling duplicate key errors from create()
โ Wrong โ unhandled MongoServerError 11000 crashes the server:
const user = await User.create({ email: 'existing@email.com', ... });
// MongoServerError: E11000 duplicate key โ unhandled โ 500 crash
โ Correct โ the global errorHandler (Chapter 7) catches err.code === 11000 and returns 409. Make sure asyncHandler wraps the controller:
const register = asyncHandler(async (req, res) => {
const user = await User.create({ email, ... });
// If duplicate โ error propagates to global errorHandler โ 409 response โ
});
Quick Reference
| Task | Code |
|---|---|
| Create and save one | await Model.create({ fields }) |
| Build then save | const d = new Model({...}); await d.save() |
| Validate without saving | await doc.validate() |
| Check field modified | doc.isModified('password') |
| Check if new | doc.isNew |
| Bulk insert | await Model.insertMany(docs, { ordered: false }) |
| Handle duplicate key | if (err.code === 11000) { ... } |