Capstone Project: Building an Order Management API

Advanced
16 min

Capstone Project: Building an Order Management API

This final chapter assembles the course into one service: an order management API built with Express, Mongoose and a MongoDB replica set (Atlas or local). It needs a searchable catalog, order placement that never oversells, per-customer history, a daily sales report and low-stock alerts. After completing it you will have applied modeling, validation, indexes, transactions, aggregation and change streams together in one codebase.

Scope and Project Layout

text
order-api/ src/ db.js connection (mongoose.connect, graceful shutdown) models/ product.js, customer.js, order.js routes/ products.js, orders.js, reports.js jobs/ dailySales.js (aggregation + $merge), lowStockWatcher.js (change stream)

Data Model

Orders reference products and customers but carry a snapshot of the customer contact and each product's name and price at purchase time (extended reference), so history survives later price changes.

javascript
const productSchema = new Schema({ sku: { type: String, required: true, unique: true, uppercase: true }, name: { type: String, required: true }, price: { type: Schema.Types.Decimal128, required: true }, stock: { type: Number, required: true, min: 0 }, tags: [String] }); productSchema.index({ name: "text" }); const orderSchema = new Schema({ customer: { _id: { type: Schema.Types.ObjectId, ref: "Customer", required: true }, name: String, email: String }, items: [{ productId: { type: Schema.Types.ObjectId, required: true }, name: String, price: Schema.Types.Decimal128, qty: { type: Number, min: 1 } }], total: { type: Schema.Types.Decimal128, required: true }, status: { type: String, enum: ["placed", "paid", "shipped", "cancelled"], default: "placed" }, placedAt: { type: Date, default: Date.now } }); orderSchema.index({ "customer._id": 1, placedAt: -1 }); // order history orderSchema.index({ status: 1, placedAt: -1 }); // fulfilment queues and reports

Placing an Order Atomically

The sample code at the top is the heart of the project. Inside withTransaction, each line item's stock is reserved with a conditional $inc whose filter includes stock: { $gte: qty }: the decrement only matches while enough stock exists, and it is atomic per document. If any line fails, the thrown error aborts the transaction and every earlier decrement is rolled back. Order.create([...], { session }) uses the array form because that overload accepts a session, and the 409 reaches the client through the error middleware.

Catalog Search and Order History

javascript
// GET /products?q=lamp&after=<lastId> router.get("/", async (req, res, next) => { try { const filter = {}; if (req.query.q) filter.$text = { $search: req.query.q }; if (req.query.after) filter._id = { $gt: new mongoose.Types.ObjectId(req.query.after) }; const products = await Product.find(filter).sort({ _id: 1 }).limit(20).lean(); res.json({ products, next: products.at(-1)?._id ?? null }); } catch (err) { next(err); } });

Order history is Order.find({ "customer._id": id }).sort({ placedAt: -1 }).limit(20).lean(), served by the compound index; confirm with explain("executionStats").

Reports and Alerts

The daily report is an aggregation that a scheduled job materialises with $merge, so the endpoint reads a tiny collection:

javascript
await Order.aggregate([ { $match: { status: { $in: ["paid", "shipped"] }, placedAt: { $gte: from, $lt: to } } }, { $group: { _id: { $dateTrunc: { date: "$placedAt", unit: "day" } }, revenue: { $sum: "$total" }, orders: { $sum: 1 } } }, { $merge: { into: "dailysales", on: "_id", whenMatched: "replace", whenNotMatched: "insert" } } ]);

Low-stock alerts come from a change stream filtered to updates that push stock below the threshold:

javascript
const stream = Product.watch( [{ $match: { operationType: "update", "updateDescription.updatedFields.stock": { $lt: 5 } } }], { fullDocument: "updateLookup" } ); for await (const change of stream) { await notifyPurchasing(change.fullDocument.sku, change.fullDocument.stock); }

Finishing Checklist

| Requirement | Technique from the course | |---|---| | No overselling under concurrent orders | conditional $inc inside a transaction | | Fast history and reports | compound indexes in ESR order, $merge materialized view | | Restorable data, least privilege | scheduled mongodump --oplog; an orders_api user with readWrite only |

Seed the database, place overlapping orders from two terminals, confirm stock never goes negative, then rehearse a backup and restore.

Quick Quiz
Question 1 of 3

Why does the order route decrement stock with `{ _id, stock: { $gte: qty } }` rather than checking stock first and then updating?

Key Takeaways

  • Snapshot the fields an order needs (extended reference) so history is immune to later price or contact changes.
  • Guard inventory with a conditional update and wrap multi-line orders in a transaction.
  • Design indexes from the queries, such as { "customer._id": 1, placedAt: -1 } for order history.
  • Materialise heavy aggregations with $merge and react to threshold events with filtered change streams.
  • Finish with validation rules, a least-privilege user and a rehearsed backup and restore.

Next lesson: What to learn next — deepen your skills with MongoDB University's Associate Developer path, Atlas Search and Vector Search, and the performance and operations material for running MongoDB at scale.

Capstone Project: Building an Order Management API - MongoDB | CodeYourCraft | CodeYourCraft