Inserting Documents — insertOne, insertMany and Bulk Operations

Every piece of data in your MERN application starts as a document inserted into MongoDB. Whether a user registers an account, an author creates a blog post, or a reader leaves a comment — every creation operation flows from your Express controller through Mongoose down to a MongoDB insert. Understanding all the insert methods, their behaviours, and how to handle their errors gives you complete control over the creation layer of your MERN stack. In this lesson you will master insertOne, insertMany, ordered vs unordered inserts, and the Mongoose equivalents you will use every day.

Insert Methods Overview

Method What It Does Returns
db.collection.insertOne(doc) Insert a single document { acknowledged, insertedId }
db.collection.insertMany([docs]) Insert multiple documents { acknowledged, insertedIds }
Model.create(doc) Mongoose: insert one, runs validators and hooks The saved document
Model.create([docs]) Mongoose: insert many with validation Array of saved documents
new Model(doc).save() Mongoose: construct then save, runs all hooks The saved document
Model.insertMany([docs]) Mongoose: bulk insert, skips some middleware { insertedDocs }
Note: Model.create() and new Model().save() both run Mongoose validators and pre/post save hooks. Model.insertMany() is faster for bulk inserts because it bypasses some middleware, but it also skips save hooks. For seeding a database with many records quickly, use insertMany(). For normal application inserts where validation and hooks matter, use create() or save().
Tip: When using Model.create([array]) in Mongoose, it runs validation on each document and throws a ValidationError on the first failure. If you want to continue inserting valid documents even when some fail validation, use Model.insertMany(docs, { ordered: false }) — MongoDB will insert all valid documents and report errors for the invalid ones without stopping the bulk operation.
Warning: MongoDB document size limit is 16 MB per document. For most use cases (blog posts, user profiles, product records) this is never a concern. However, if you are embedding arrays that grow over time — like all comments inside a post, or all activity logs inside a user — the document can approach this limit. For unbounded arrays, store items in a separate collection rather than embedding them.

insertOne — Single Document

// mongosh — raw driver
db.posts.insertOne({
  title:     "My First Blog Post",
  slug:      "my-first-blog-post",
  body:      "Content of the post...",
  tags:      ["mern", "beginner"],
  published: false,
  viewCount: 0,
  createdAt: new Date(),
});
// Returns:
// { acknowledged: true, insertedId: ObjectId("64a1f2b3c8e4d5f6a7b8c9d0") }

// ── Mongoose equivalent ────────────────────────────────────────────────────────
const post = await Post.create({
  title:  "My First Blog Post",
  body:   "Content of the post...",
  tags:   ["mern", "beginner"],
  author: req.user.id,
});
// Returns the full document including _id, timestamps, and any defaults applied
console.log(post._id);        // ObjectId
console.log(post.createdAt);  // Date — set by timestamps: true
console.log(post.published);  // false — schema default applied

insertMany — Multiple Documents

// mongosh — insert multiple posts at once
db.posts.insertMany([
  { title: "Post One",   body: "Content 1", published: true,  createdAt: new Date() },
  { title: "Post Two",   body: "Content 2", published: false, createdAt: new Date() },
  { title: "Post Three", body: "Content 3", published: true,  createdAt: new Date() },
]);
// Returns:
// {
//   acknowledged: true,
//   insertedIds: {
//     '0': ObjectId("64a1f2b3..."),
//     '1': ObjectId("64a1f2b4..."),
//     '2': ObjectId("64a1f2b5...")
//   }
// }

// ── Mongoose equivalent — create() with array ──────────────────────────────────
const posts = await Post.create([
  { title: "Post One",   body: "Content 1", published: true,  author: adminId },
  { title: "Post Two",   body: "Content 2", published: false, author: adminId },
]);
// Each document runs validation individually
// If document 1 fails validation — error thrown, document 2 is not inserted

new Model().save() — Construct Then Save

// Useful when you need to modify the document before saving
const post = new Post({
  title:  "Draft Post",
  body:   "Work in progress...",
  author: req.user.id,
});

// You can modify the document before saving
post.slug = generateSlug(post.title); // custom slug generation
post.tags = extractTagsFromBody(post.body);

// Validate without saving first
await post.validate(); // throws ValidationError if invalid

// Then save
const saved = await post.save(); // runs pre('save') and post('save') hooks
console.log(saved._id); // available after save

Bulk Inserts for Seeding

// server/src/scripts/seedPosts.js
// Fast bulk insert using insertMany — skips save hooks for performance
const mongoose = require('mongoose');
const Post     = require('../models/Post');

const seedPosts = async () => {
  await mongoose.connect(process.env.MONGODB_URI);

  // Clear existing posts first
  await Post.deleteMany({});
  console.log('Cleared existing posts');

  const posts = Array.from({ length: 50 }, (_, i) => ({
    title:     `Sample Post ${i + 1}`,
    body:      `Content for post ${i + 1}. `.repeat(20),
    slug:      `sample-post-${i + 1}`,
    tags:      ['sample', i % 2 === 0 ? 'even' : 'odd'],
    published: true,
    author:    new mongoose.Types.ObjectId(), // placeholder author ID
    viewCount: Math.floor(Math.random() * 500),
    createdAt: new Date(Date.now() - i * 24 * 60 * 60 * 1000), // spaced by 1 day
  }));

  // Model.insertMany — fast, skips save hooks, sets { ordered: true } by default
  const result = await Post.insertMany(posts, { ordered: false });
  console.log(`Seeded ${result.length} posts`);

  await mongoose.connection.close();
};

seedPosts().catch(console.error);

Handling Duplicate Key Errors

// A unique index on 'slug' means inserting a duplicate slug throws an error
try {
  await Post.create({ title: 'Existing Post', slug: 'existing-slug', ... });
} catch (err) {
  if (err.code === 11000) {
    // MongoServerError: E11000 duplicate key error collection: blogdb.posts
    const field = Object.keys(err.keyValue)[0]; // 'slug'
    console.error(`Duplicate value for field: ${field}`);
    // In an Express controller: throw new AppError(`${field} already exists`, 409)
  } else {
    throw err; // re-throw unexpected errors
  }
}

Common Mistakes

Mistake 1 — Not awaiting insert operations

❌ Wrong — missing await on an async insert:

const post = Post.create({ title: 'Post', body: '...' }); // missing await
console.log(post._id); // undefined — post is a Promise, not a document

✅ Correct:

const post = await Post.create({ title: 'Post', body: '...' }); // ✓
console.log(post._id); // ObjectId — document is saved and returned

Mistake 2 — Including the _id in insertMany when it may clash

❌ Wrong — supplying the same _id in a retry loop:

await Post.insertMany([{ _id: fixedId, title: '...' }]); // first call succeeds
await Post.insertMany([{ _id: fixedId, title: '...' }]); // duplicate key error

✅ Correct — omit _id and let MongoDB generate unique ObjectIds automatically.

Mistake 3 — Using insertMany with ordered: true and one bad document

❌ Wrong — one invalid document stops all subsequent inserts:

await Post.insertMany([
  { title: 'Good Post', body: '...', author: id },   // inserted
  { body: 'Missing title' },                          // fails validation → stops here
  { title: 'Another Post', body: '...', author: id }, // never inserted
], { ordered: true }); // default — stops on first error

✅ Correct — use ordered: false to insert all valid documents and collect errors:

const result = await Post.insertMany(docs, { ordered: false });
// Inserts all valid documents, reports errors for invalid ones without stopping

Quick Reference

Task Mongoose Code
Create one document await Model.create({ fields })
Create many documents await Model.create([{ doc1 }, { doc2 }])
Construct and save const doc = new Model({...}); await doc.save()
Fast bulk insert await Model.insertMany(docs, { ordered: false })
Check duplicate key if (err.code === 11000) { ... }
Get inserted ID const doc = await Model.create({...}); doc._id

🧠 Test Yourself

You call Post.insertMany(docs) with 100 documents. Document 47 has a missing required field. What happens with the default ordered: true behaviour?