Real CRUD Applications
Most real applications are 90% CRUD: a SaaS dashboard saves a user, lists tickets, updates an order, deletes a draft.
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
upsertwith a natural key (email, slug, external id) instead of "find then insert". - Soft delete — set
deletedAtinstead ofdeleteOnefor any business-critical entity. - Audit trail — append
{ at, by, action }to a history array on every write. - Pagination — cursor-based (
_idorcreatedAt), neverskip()for big lists.
Informative example
User-management service — the four operations a real backend exposes:
// CREATE — idempotent signupawait db.users.updateOne({ email: dto.email.toLowerCase() },{ $setOnInsert: { email: dto.email.toLowerCase(), passwordHash, createdAt: new Date(), roles: ["user"] } },{ upsert: true });// READ — paginated, projected, indexedconst users = await db.users.find({ status: "active", deletedAt: null },{ projection: { passwordHash: 0 } }).sort({ _id: -1 }).limit(20).toArray();// UPDATE — atomic, auditedawait 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, recoverableawait 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
findOneAndUpdatewithreturnDocument: "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.