MongoDB Tutorial 0/120 lessons ~6 min read Lesson 45

    Real CRUD Applications

    Most real applications are 90% CRUD: a SaaS dashboard saves a user, lists tickets, updates an order, deletes a draft.

    Course progress0%
    Focus
    6 guided sections
    Practice signal
    Examples included
    Career prep
    Foundation builder

    Introduction

    Most real applications are 90% CRUD: a SaaS dashboard saves a user, lists tickets, updates an order, deletes a draft. The patterns you've learned (insertOne, find, updateMany, deleteOne) are exactly what powers production systems — wrapped in services, validated with schemas, and protected by indexes.

    This lesson walks through three concrete CRUD systems — a user management API, a blog content service, and a shopping cart — showing the data model, the queries that hit MongoDB, and the gotchas teams run into when these systems hit thousands of users.

    Understanding the topic

    Anatomy of production-grade CRUD:

    • Repository layer — wrap every collection in a class/module so business code never calls db.collection() directly.
    • Input validation — Zod / Joi / class-validator before the document ever reaches MongoDB.
    • Idempotency — use upsert with a natural key (email, slug, external id) instead of "find then insert".
    • Soft delete — set deletedAt instead of deleteOne for any business-critical entity.
    • Audit trail — append { at, by, action } to a history array on every write.
    • Pagination — cursor-based (_id or createdAt), never skip() for big lists.

    Informative example

    User-management service — the four operations a real backend exposes:

    js
    // CREATE — idempotent signup
    await db.users.updateOne(
    { email: dto.email.toLowerCase() },
    { $setOnInsert: { email: dto.email.toLowerCase(), passwordHash, createdAt: new Date(), roles: ["user"] } },
    { upsert: true }
    );
    // READ — paginated, projected, indexed
    const users = await db.users.find(
    { status: "active", deletedAt: null },
    { projection: { passwordHash: 0 } }
    ).sort({ _id: -1 }).limit(20).toArray();
    // UPDATE — atomic, audited
    await db.users.updateOne(
    { _id, version: currentVersion }, // optimistic lock
    { $set: { name, updatedAt: new Date() }, $inc: { version: 1 },
    $push: { history: { at: new Date(), by: actor, action: "rename" } } }
    );
    // DELETE — soft, recoverable
    await db.users.updateOne({ _id }, { $set: { deletedAt: new Date(), status: "deleted" } });

    Real-world use

    Stripe's customer object, Notion's pages, and Linear's issues are all CRUD-driven document stores built on top of these exact patterns: upsert on natural key, soft delete with timestamp, optimistic concurrency with a version counter, and audit log in an embedded array.

    Best practices

    • Always create the index before the query goes live — never debug a slow CRUD endpoint in production first.
    • Use findOneAndUpdate with returnDocument: "after" to avoid a second round-trip.
    • Wrap multi-document changes in a transaction when the data must stay consistent (cart + inventory).
    • Return the new document from your API — clients hate guessing what was saved.

    Common mistakes

    • Using skip(n) for pagination — gets O(n) slow past page 100.
    • Forgetting { projection: { passwordHash: 0 } } and leaking hashes in API responses.
    • Hard-deleting business entities (orders, invoices) — auditors will not be happy.
    Ready to mark this lesson complete?Track your journey across the entire course.