Transaction-Safe Mongoose Dashboard
Build a shop dashboard with Mongoose transactions, discriminators, and reports.
Introduction
30 Second Summary
A checkout can look successful while leaving a shop's records out of sync. One failed order can reduce stock without saving the purchase.
In this project, you will build a browser shop dashboard backed by Mongoose on MongoDB Atlas. You'll reproduce that partial-write bug before using an ACID transaction to roll it back safely.
What You'll Build
You'll demo a broken checkout beside a transaction-safe checkout that keeps every visible stock count aligned with its saved orders.
By the end of this project, you'll have:
- A live inventory dashboard where stock falls as valid delivery or pickup orders appear.
- A polymorphic order view where delivery and pickup records share one collection through Mongoose discriminators. Each record shows a lowercase email, calculated total, and reusable summary() output.
- A rollback demonstration that exposes a partial write before restoring consistency with a transaction. A sales report powered by an aggregation pipeline then shows paid order counts by type.
- Secret Mission: Add a GiftOrder discriminator that reuses the existing transaction. Watch it appear automatically in the sales report.
Are there any prerequisites?
You need basic JavaScript syntax plus an existing MongoDB Atlas account with a cluster.
The local workflow uses Windows with Node.js, npm, Visual Studio Code, and Microsoft Edge.
Before We Start
Before the hands-on work begins, lock in what checkout must protect and why it matters. This gives every later database behavior a clear purpose.
Connect the Starter App to MongoDB Atlas
Your checkout experiment needs a real database connection before you can test failed writes or rollback. MongoDB Atlas provides the transaction-capable replica set that supports the later checkout tests.
You will verify Node.js v24.21.0 LTS. You will then prepare a starter app with Express 5.2.1, Mongoose 9.10.4, Visual Studio Code, and Microsoft Edge.
In this step, get ready to:
- Verify the required Windows tools.
- Create the starter project with its pinned dependencies.
- Launch the starter dashboard in Microsoft Edge.
Verify your Windows tools
Node.js runs the starter server. PowerShell lets you confirm that Windows can find the required version before you create any files.
- Press the Windows key to open Windows search.
- Type PowerShell into the search field.
- Press Enter to open PowerShell.
- Check the installed Node.js version by running this command:
node --version
What does this command check?
The --version flag asks Node.js to print its installed version. This project uses v24.21.0 LTS so every later command runs in the pinned environment.
✔️ I see the required LTS version
You should see v24.21.0. Your Node.js runtime is ready for the starter app.
ⓧ I see an older version
An older version puts your environment outside this project's tested setup. Install the pinned LTS release before continuing.
- Visit the official Node.js download page.
- Download Node.js v24.21.0 LTS for Windows.
- Run the downloaded installer with its default options.
- Close the current PowerShell window after installation.
- Open a fresh PowerShell window through Windows search.
- Repeat the version check above.
ⓧ Command not found
Windows cannot currently find Node.js. Install the pinned release to add the runtime to your environment.
- Visit the official Node.js download page.
- Download Node.js v24.21.0 LTS for Windows.
- Run the downloaded installer with its default options.
- Close the current PowerShell window after installation.
- Open a fresh PowerShell window through Windows search.
- Repeat the version check above.
The finished dashboard runs in Microsoft Edge. A quick launch check confirms the browser is available before the local server takes over its address bar.
- Press the Windows key to open Windows search.
- Type Microsoft Edge into the search field.
- Press Enter to launch the browser.
✔️ Microsoft Edge opens
Microsoft Edge is ready. Keep the browser available for the dashboard check at the end of this step.
ⓧ Microsoft Edge is missing
Install the Windows browser from Microsoft's official download page before continuing.
- Visit the official Microsoft Edge download page.
- Download Microsoft Edge for Windows.
- Run the downloaded installer.
- Launch Microsoft Edge through Windows search after installation.
Create the starter project
The project folder keeps the server code beside its browser files. Its dependency manifest pins every package so your environment matches the tested build.
- Return to the PowerShell window from earlier.
- Move to your Desktop by running this command:
Set-Location -Path "$env:USERPROFILE\Desktop"
What does this command do?
PowerShell changes its current location to your Windows Desktop. The project folder you create next will be easy to find in File Explorer.
- Create the mongoose-advanced-shop folder and enter it by running these commands:
New-Item -Path "$Env:USERPROFILE\Desktop\mongoose-advanced-shop" -ItemType Directory
Set-Location -Path "$Env:USERPROFILE\Desktop\mongoose-advanced-shop"
What do these commands do?
The first command creates mongoose-advanced-shop on your Desktop. The second command moves PowerShell into that folder so later files land in the correct place.
- Open the current folder in Visual Studio Code by running this command:
code .
What does this command do?
The code command starts Visual Studio Code. The dot tells it to open the current mongoose-advanced-shop folder.
Your project now has a permanent home. You should see mongoose-advanced-shop at the top of the Explorer sidebar.
Visual Studio Code did not open?
The folder still exists on your Desktop if the command-line shortcut is unavailable. Launch Visual Studio Code through Windows search and use its folder picker to select mongoose-advanced-shop.
Ask for help with the terminal shortcut:
- Click the New File icon in the Explorer sidebar.
- Name the new file package.json.
- Define the project scripts and pinned dependencies by replacing the file contents with this code:
{
"name": "mongoose-advanced-shop",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node --env-file=.env server.js"
},
"dependencies": {
"express": "5.2.1",
"mongoose": "9.10.4"
}
}
What does this file configure?
- The start script loads values from .env before running server.js.
- The dependency list pins Express to 5.2.1.
- The dependency list pins Mongoose to 9.10.4.
- The private setting prevents accidental publication as a public npm package.
- Save package.json by pressing Ctrl+S.
- Open the integrated terminal by pressing Ctrl+`.
- Install the pinned dependencies by running this command:
npm install
What does this command install?
The command reads package.json and installs its dependencies into node_modules. It also records the resolved dependency tree in package-lock.json.
Your pinned server libraries are now installed. You should see node_modules and package-lock.json in the Explorer sidebar.
Did the dependency installation fail?
Confirm the integrated terminal is inside mongoose-advanced-shop. Confirm that package.json contains valid JSON with matching braces.
Ask for help with your installation output.
Configuration files deserve protection before you add the database connection. A .gitignore file tells Git to exclude generated dependencies and local credentials.
- Click the New File icon in the Explorer sidebar.
- Name the new file .gitignore.
- Exclude the dependency folder and environment file by adding this code:
node_modules/
.env
What does this file protect?
The first line excludes downloaded packages that npm can recreate. The second line keeps the private Atlas connection string out of source control.
- Save .gitignore by pressing Ctrl+S.
- Confirm .gitignore appears beside package.json in the Explorer sidebar.
Cannot see .gitignore?
Confirm the filename starts with a dot. Remove any automatic .txt extension from the name.
Ask for help checking the filename.
The browser needs one visible page for the connection check. Express serves this page directly from the public folder.
- Click the New Folder icon in the Explorer sidebar.
- Name the folder public.
- Right-click the new public folder.
- Select New File.
- Name the file index.html.
- Create the starter dashboard page by replacing the file contents with this code:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Advanced Mongoose Shop Lab</title>
</head>
<body>
<h1>Advanced Mongoose Shop Lab</h1>
</body>
</html>
What does this page provide?
The document title names the browser tab. The visible heading gives you a concrete signal that Express served the correct file.
- Save public/index.html by pressing Ctrl+S.
- Confirm index.html appears inside public in the Explorer sidebar.
Is index.html in the wrong place?
The final path must be public/index.html inside mongoose-advanced-shop. Move the file into public if it appears beside the folder.
Ask for help checking the file layout.
The server connects to Atlas before it listens for browser requests. This ordering makes the startup message a reliable database connection check.
- Click the New File icon in the Explorer sidebar.
- Name the new file server.js.
- Create the starter Express server by replacing the file contents with this code:
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const port = 3000;
app.use(express.static('public'));
async function start() {
const uri = process.env.MONGODB_URI;
if (!uri || uri === 'your-mongodb-atlas-connection-string-here') {
throw new Error('Replace the MONGODB_URI placeholder in .env');
}
await mongoose.connect(uri, { dbName: 'mongoose_advanced_lab' });
console.log('Connected to MongoDB Atlas');
app.listen(port, () => {
console.log(`Shop lab running at http://localhost:${port}`);
});
}
start().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
What does this server do?
- The imports load Express and Mongoose from node_modules.
- The static middleware serves files from public.
- The startup function reads MONGODB_URI from the environment.
- The connection selects the mongoose_advanced_lab database.
- The app starts listening on port 3000 after Atlas accepts the connection.
- Save server.js by pressing Ctrl+S.
- Confirm server.js appears beside package.json in the Explorer sidebar.
Does server.js show a syntax warning?
Check each quote and closing brace against the code above. Confirm the file is named server.js with no extra extension.
Ask for help checking the starter server.
Launch the Atlas-connected app
Atlas must recognize your network before it accepts the connection. It also needs a database user whose credentials appear in the SRV connection string.
Why use Atlas for this project?
Multi-document transactions require a replica set. Atlas provides that deployment model without adding local replica-set administration to your Windows setup.
- Visit the official MongoDB Atlas sign-in page in Microsoft Edge.
- Sign in to your existing Atlas account.
- Select the project containing your existing cluster.
- Click Connect for that cluster.
- Complete Setup connection security if Atlas displays it.
- Confirm your current IP address appears on the IP Access List.
- Confirm a Database User exists for the connection.
Keep Atlas access narrow
An entry of 0.0.0.0/0 permits connections from anywhere. Use your current trusted IP address for this project.
- Click Choose Your Connection Method.
- Select Drivers.
- Copy the SRV string under Use this connection string in your application.
- Return to Visual Studio Code.
- Click the New File icon in the Explorer sidebar.
- Name the new file .env.
- Add the environment variable in this format:
MONGODB_URI=your-mongodb-atlas-connection-string-here
What belongs in this file?
The value after MONGODB_URI= holds the SRV connection string copied from Atlas. Node.js loads this value before server.js starts.
The SRV string contains database access details. Keep the value inside .env and avoid sharing screenshots of that file.
- Replace your-mongodb-atlas-connection-string-here with the SRV string you copied from Atlas.
- Replace the password placeholder inside the copied string with your database user's password.
- Save .env by pressing Ctrl+S.
- Confirm the value after MONGODB_URI= now contains your Atlas SRV connection string.
- Confirm .env appears beside .gitignore in the Explorer sidebar.
Before you start the server, do you expect Atlas to accept the connection or reject it?
- Start the connected app by running this command in the integrated terminal:
npm start
What does this command start?
npm runs the start script from package.json. Node.js loads .env before the server connects to Atlas.
You should see Connected to MongoDB Atlas in the terminal. You should also see Shop lab running at http://localhost:3000.
Did the Atlas connection fail?
- Confirm .env sits beside package.json.
- Confirm the MONGODB_URI value no longer contains the starter placeholder.
- Confirm your current IP address remains on the Atlas IP Access List.
- Confirm the database user credentials match the values in the SRV string.
Ask for help with the connection failure.
- Return to Microsoft Edge.
- Enter http://localhost:3000 in the address bar.
- Press Enter to load the starter dashboard.
You should see Advanced Mongoose Shop Lab as the visible page heading. That is your first full-stack checkpoint because the page only starts after the Atlas connection succeeds.
Does the page fail to load?
Keep the PowerShell process running while Edge loads the page. Confirm the address uses port 3000.
Ask for help checking the local server.
✔️ Awesome, I've got everything!
Your starter app is connected to Atlas. Double-check that every file is saved before moving on.
ⓧ I'd like to double check the full code
Compare each file with these references. Keep your real Atlas SRV value in .env when checking the redacted environment example.
{
"name": "mongoose-advanced-shop",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node --env-file=.env server.js"
},
"dependencies": {
"express": "5.2.1",
"mongoose": "9.10.4"
}
}
Why package.json matters
This file pins the two server dependencies. It also defines the startup command that loads .env.
node_modules/
.env
Why .gitignore matters
This file excludes downloaded packages and private environment configuration from source control.
MONGODB_URI=your-mongodb-atlas-connection-string-here
Why the URI is redacted
Your saved file contains your private Atlas SRV value. This reference uses the project placeholder so your credentials remain private.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Advanced Mongoose Shop Lab</title>
</head>
<body>
<h1>Advanced Mongoose Shop Lab</h1>
</body>
</html>
Why index.html matters
This file supplies the browser title and visible heading served from the public folder.
const express = require('express');
const mongoose = require('mongoose');
const app = express();
const port = 3000;
app.use(express.static('public'));
async function start() {
const uri = process.env.MONGODB_URI;
if (!uri || uri === 'your-mongodb-atlas-connection-string-here') {
throw new Error('Replace the MONGODB_URI placeholder in .env');
}
await mongoose.connect(uri, { dbName: 'mongoose_advanced_lab' });
console.log('Connected to MongoDB Atlas');
app.listen(port, () => {
console.log(`Shop lab running at http://localhost:${port}`);
});
}
start().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
Why server.js matters
This file connects Mongoose to mongoose_advanced_lab before Express serves the browser page on port 3000.
Your starter dashboard now reaches MongoDB Atlas and renders through Express. Next, you will give orders their hooks, virtual output, reusable methods, and discriminator subtypes.
Add Advanced Order Models
The starter Express app already reaches MongoDB Atlas. The shop now needs a model layer that keeps product data consistent across every dashboard action.
Advanced Mongoose schemas place normalization and calculated output beside the data they govern. Discriminators let delivery orders and pickup orders share one collection while enforcing subtype-specific fields.
In this step, get ready to:
- Define the product schema and shared order behavior.
- Create delivery and pickup order discriminators.
- Connect reset and state routes to a browser dashboard.
Define shared order behavior
The product schema protects the inventory fields used during checkout. The base order schema defines the fields and serialization behavior shared by every order subtype.
- Create models.js beside server.js by using the new-file control in the VS Code file sidebar.
- Add the product schema and base order schema by pasting this code into models.js:
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
sku: { type: String, required: true, unique: true },
name: { type: String, required: true },
price: { type: Number, required: true, min: 0 },
stock: { type: Number, required: true, min: 0 }
});
const orderSchema = new mongoose.Schema(
{
customerName: { type: String, required: true },
customerEmail: { type: String, required: true },
productSku: { type: String, required: true },
quantity: { type: Number, required: true, min: 1 },
unitPrice: { type: Number, required: true, min: 0 },
status: { type: String, enum: ['paid'], default: 'paid' }
},
{
discriminatorKey: 'kind',
collection: 'orders',
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
What Do These Schemas Define?
- The product schema requires a unique sku value.
- The product schema prevents negative price and stock values.
- The order schema stores the customer details and purchased item data shared by every subtype.
- The kind discriminator key records the concrete order type in the shared orders collection.
- The serialization options include virtual values in object and JSON output.
- Save models.js.
- Check the current schema syntax by running this command in the integrated terminal:
node --check models.js
What Does This Check Prove?
The command checks the JavaScript syntax without executing the model file. A return to the terminal prompt without a syntax error confirms that both schema declarations are valid.
Order behavior belongs on the schema that owns the data. Hooks manage lifecycle events while a virtual and an instance method provide calculated document output.
- Place your cursor below the closing ); for orderSchema.
- Add the order middleware and document behavior by pasting this code:
orderSchema.pre('validate', function normalizeCustomerEmail() {
if (this.customerEmail) {
this.customerEmail = this.customerEmail.trim().toLowerCase();
}
});
orderSchema.post('save', function logSavedOrder(doc) {
console.log(`[audit] saved ${doc.kind} ${doc._id}`);
});
orderSchema.virtual('total').get(function calculateTotal() {
return this.quantity * this.unitPrice + (this.shippingFee || 0);
});
orderSchema.methods.summary = function summary() {
return `${this.kind}: ${this.quantity} x ${this.productSku} = $${this.total.toFixed(2)}`;
};
How Does the Order Behavior Work?
- The pre('validate') hook trims the customer email before converting it to lowercase.
- The post('save') hook prints an audit line after an order is saved.
- The total virtual calculates item cost plus any shipping fee.
- The summary() instance method formats one hydrated order as a reusable description.
The base model must be compiled after its hooks and methods are defined. This ordering gives every subtype access to the completed behavior.
- Place your cursor below the summary() method.
- Compile the models and export them by pasting this code:
const Product = mongoose.model('Product', productSchema);
const Order = mongoose.model('Order', orderSchema);
const DeliveryOrder = Order.discriminator(
'DeliveryOrder',
new mongoose.Schema({
deliveryAddress: { type: String, required: true },
shippingFee: { type: Number, default: 5, min: 0 }
})
);
const PickupOrder = Order.discriminator(
'PickupOrder',
new mongoose.Schema({
pickupCode: { type: String, required: true }
})
);
module.exports = {
Product,
Order,
DeliveryOrder,
PickupOrder
};
Why Use Discriminators?
Both subtypes inherit the fields and behavior of the base Order model. Their documents stay together in the orders collection.
A delivery order requires deliveryAddress. A pickup order requires pickupCode.
- Save models.js.
- Check the completed model file by running this command:
node --check models.js
What Should the Check Confirm?
A return to the terminal prompt without a syntax error confirms that the schemas and model declarations form valid JavaScript. The model file is ready for the server to import.
Seeing a Model Syntax Error?
Start with the first line number reported in the terminal. Check the matching brackets around orderSchema before inspecting later declarations.
Confirm that the hooks appear before mongoose.model('Order', orderSchema). Help me debug my model file.
Connect the models to Express
The server needs routes that seed a predictable product and return the current database state. Hydrated order documents let the state helper call summary() before sending each response.
- Return to the integrated terminal that is running the starter server.
- Press Ctrl+C to stop the server.
- Switch back to the existing server.js tab.
- Select all code in server.js.
- Replace the selected code with this server foundation:
const express = require('express');
const mongoose = require('mongoose');
const {
Product,
Order,
DeliveryOrder,
PickupOrder
} = require('./models');
const app = express();
const port = 3000;
const sku = 'MUG-001';
app.use(express.json());
app.use(express.static('public'));
function asyncHandler(handler) {
return function wrappedHandler(req, res, next) {
Promise.resolve(handler(req, res, next)).catch(next);
};
}
What Does the Server Foundation Do?
- The model import gives the server access to the product model and all order models.
- The JSON middleware prepares request bodies for later checkout routes.
- Static file serving keeps the existing public directory available to the browser.
- The asyncHandler() wrapper forwards rejected route promises to the error middleware.
- Save server.js.
- Check the server foundation by running this command:
node --check server.js
What Does the First Server Check Show?
The syntax check confirms that the imports and helper function are valid. It leaves the server stopped while you add the routes.
- Place your cursor below the closing brace of asyncHandler().
- Add the state helper and state route by pasting this code:
async function getState() {
const product = await Product.findOne({ sku });
const orders = await Order.find({}).sort({ createdAt: 1 });
return {
product: product ? product.toJSON() : null,
orders: orders.map(order => ({
...order.toJSON(),
summary: order.summary()
}))
};
}
app.get('/api/state', asyncHandler(async (req, res) => {
res.json(await getState());
}));
How Does State Reach the Dashboard?
- The getState() helper reads the product.
- The helper sorts orders by creation time.
- Each order is serialized with virtual values included.
- The helper adds the result of summary() to each order response.
- The /api/state route returns the complete state as JSON.
- Place your cursor below the /api/state route.
- Add the reset route by pasting this code:
app.post('/api/reset', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
await Product.create({
sku,
name: 'Workshop Mug',
price: 20,
stock: 3
});
res.json({ message: 'Lab reset', state: await getState() });
}));
What Does Reset Create?
The reset route clears the existing lab documents. It seeds a Workshop Mug with SKU MUG-001.
The seeded mug has a price of 20 and a stock level of 3.
- Save server.js.
- Check the state and reset routes by running this command:
node --check server.js
What Does the Route Check Confirm?
A return to the prompt without a syntax error confirms that both routes are valid JavaScript. The server is ready for cleanup and startup behavior.
The cleanup route removes the documents created by this lab. Central error middleware keeps unexpected route failures in one response format.
- Place your cursor below the reset route.
- Add the cleanup route and error middleware by pasting this code:
app.post('/api/cleanup', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
res.json({ message: 'Lab data deleted', state: await getState() });
}));
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Unexpected server error' });
});
What Does This Section Handle?
The cleanup route removes product and order documents created by the lab. The error middleware logs unexpected failures before returning a server error response.
Startup connects to the existing database before preparing both collections. Explicit collection creation gives the later transaction workflow a stable database structure.
- Place your cursor below the error middleware.
- Add the startup function by pasting this code:
async function start() {
const uri = process.env.MONGODB_URI;
if (!uri || uri === 'your-mongodb-atlas-connection-string-here') {
throw new Error('Replace the MONGODB_URI placeholder in .env');
}
await mongoose.connect(uri, { dbName: 'mongoose_advanced_lab' });
await Product.createCollection();
await Order.createCollection();
console.log('Connected to MongoDB Atlas');
app.listen(port, () => {
console.log(`Shop lab running at http://localhost:${port}`);
});
}
start().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
Why Create the Collections Here?
The app creates the products collection through Product. It creates the shared orders collection through Order.
Delivery and pickup documents use the same orders collection. The kind field preserves each subtype.
- Save server.js.
Before you start the updated server, do you expect the existing Atlas connection to survive the new model imports?
- Start the updated server by running this command:
npm start
What Does the Startup Prove?
The start script loads the existing environment file before running server.js. A successful connection proves that the new model file compiles during startup.
You should see Connected to MongoDB Atlas followed by the local shop URL. The products and orders collections are now available.
Does the Updated Server Fail to Start?
Check that models.js sits beside server.js inside mongoose-advanced-shop. Confirm that all four exported names match the server import.
Keep the connection string private while describing the terminal output. Help me debug the server startup.
Build the dashboard controls
The browser needs controls that call the new routes and panels that display their responses. This creates a visible loop from the page to Express and MongoDB Atlas.
- Switch back to the existing public/index.html tab.
- Select all code in public/index.html.
- Replace the selected code with this dashboard shell:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Advanced Mongoose Shop Lab</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
background: #10131a;
color: #f3f5f7;
}
body {
max-width: 1000px;
margin: 0 auto;
padding: 32px 20px;
}
</style>
</head>
<body>
<h1>Advanced Mongoose Shop Lab</h1>
<p>Run each action in order and watch inventory, orders, rollback, virtuals, and aggregation results.</p>
</body>
</html>
What Does the Dashboard Shell Provide?
The page keeps the existing shop title while adding a dark color scheme. The body width leaves room for readable database output.
- Save public/index.html.
- Return to the Microsoft Edge tab from earlier.
- Refresh the local page.
You should see the shop heading on a dark page. The introduction now names the database behavior the dashboard will expose.
- Place your cursor above </style> in public/index.html.
- Add the panel and button styling by pasting this code:
.panel {
background: #1b2230;
border: 1px solid #34405a;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
button {
border: 0;
border-radius: 8px;
padding: 10px 14px;
background: #67d8a4;
color: #102016;
font-weight: 700;
cursor: pointer;
}
What Does This Styling Control?
Each dashboard section uses a bordered panel. The action buttons use a flexible layout that wraps on narrower screens.
- Place your cursor below the introductory paragraph in the page body.
- Add the action controls and output panels by pasting this markup:
<section class="panel">
<h2>Lab actions</h2>
<div class="buttons">
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
</div>
</section>
<section class="panel">
<h2>Current database state</h2>
<pre id="state">Loading...</pre>
</section>
<section class="panel">
<h2>Latest response</h2>
<pre id="output">Choose an action.</pre>
</section>
What Will These Panels Show?
- The action panel calls the reset and cleanup routes.
- The state panel displays the latest product and order documents.
- The response panel displays the result of the latest action.
- Save public/index.html.
- Refresh the local page in Edge.
You should see two action buttons above the Current database state panel. You should also see the Latest response panel.
The cleanup action needs a distinct warning color. The output areas also need enough contrast for formatted database responses.
- Place your cursor below the closing brace of the button style.
- Add the cleanup and output styling by pasting this code:
button.danger {
background: #ff8d8d;
color: #2d0c0c;
}
pre {
min-height: 100px;
overflow: auto;
background: #0b0e14;
border-radius: 8px;
padding: 14px;
white-space: pre-wrap;
}
Why Distinguish the Cleanup Action?
The red treatment signals that Delete Lab Data removes the lab documents. The darker output areas keep formatted responses readable.
Browser requests complete the full stack loop. One function reads database state while another sends an action before refreshing that state.
- Place your cursor above </body> in public/index.html.
- Add the dashboard request functions by pasting this script:
<script>
const state = document.querySelector('#state');
const output = document.querySelector('#output');
async function refreshState() {
const response = await fetch('/api/state');
const data = await response.json();
state.textContent = JSON.stringify(data, null, 2);
}
async function runAction(path, body) {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
};
if (body !== undefined) {
options.body = JSON.stringify(body);
}
const response = await fetch(path, options);
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
await refreshState();
}
refreshState().catch(error => {
output.textContent = error.message;
});
</script>
How Does the Browser Stay in Sync?
- The two DOM references point to the state and response panels.
- The refreshState() function requests the latest database state.
- The runAction() function sends a POST request.
- The action function refreshes the visible state after receiving a response.
- The initial state request runs when the page loads.
- Save public/index.html.
- Refresh the local page in Edge.
The Loading message should be replaced by a response from /api/state. You should see the current product value and an orders array.
Does the State Panel Stay on Loading?
Confirm that the server terminal still shows the local shop URL. Check that the script appears above </body> in public/index.html.
A browser request failure appears in the Latest response panel. Help me debug the dashboard request.
Before you reset the lab, what stock level do you expect the seeded product to show?
- Click Reset Lab in the action panel.
You should see Lab reset in the Latest response panel. The Current database state panel should show Workshop Mug with stock 3 and an empty orders array.
That full stack loop is working. Your dashboard can now seed inventory and render database state through reusable Mongoose models.
✔️ Awesome, I've got everything!
Great work. Double check that models.js, server.js, and public/index.html are saved.
ⓧ I'd like to double check the full code
Compare your three updated files with these completed versions.
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
sku: { type: String, required: true, unique: true },
name: { type: String, required: true },
price: { type: Number, required: true, min: 0 },
stock: { type: Number, required: true, min: 0 }
});
const orderSchema = new mongoose.Schema(
{
customerName: { type: String, required: true },
customerEmail: { type: String, required: true },
productSku: { type: String, required: true },
quantity: { type: Number, required: true, min: 1 },
unitPrice: { type: Number, required: true, min: 0 },
status: { type: String, enum: ['paid'], default: 'paid' }
},
{
discriminatorKey: 'kind',
collection: 'orders',
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
orderSchema.pre('validate', function normalizeCustomerEmail() {
if (this.customerEmail) {
this.customerEmail = this.customerEmail.trim().toLowerCase();
}
});
orderSchema.post('save', function logSavedOrder(doc) {
console.log(`[audit] saved ${doc.kind} ${doc._id}`);
});
orderSchema.virtual('total').get(function calculateTotal() {
return this.quantity * this.unitPrice + (this.shippingFee || 0);
});
orderSchema.methods.summary = function summary() {
return `${this.kind}: ${this.quantity} x ${this.productSku} = $${this.total.toFixed(2)}`;
};
const Product = mongoose.model('Product', productSchema);
const Order = mongoose.model('Order', orderSchema);
const DeliveryOrder = Order.discriminator(
'DeliveryOrder',
new mongoose.Schema({
deliveryAddress: { type: String, required: true },
shippingFee: { type: Number, default: 5, min: 0 }
})
);
const PickupOrder = Order.discriminator(
'PickupOrder',
new mongoose.Schema({
pickupCode: { type: String, required: true }
})
);
module.exports = {
Product,
Order,
DeliveryOrder,
PickupOrder
};
What Should Match in models.js?
The hooks and document behavior appear before the base Order model is compiled. Both discriminators are exported with the Product and Order models.
const express = require('express');
const mongoose = require('mongoose');
const {
Product,
Order,
DeliveryOrder,
PickupOrder
} = require('./models');
const app = express();
const port = 3000;
const sku = 'MUG-001';
app.use(express.json());
app.use(express.static('public'));
function asyncHandler(handler) {
return function wrappedHandler(req, res, next) {
Promise.resolve(handler(req, res, next)).catch(next);
};
}
async function getState() {
const product = await Product.findOne({ sku });
const orders = await Order.find({}).sort({ createdAt: 1 });
return {
product: product ? product.toJSON() : null,
orders: orders.map(order => ({
...order.toJSON(),
summary: order.summary()
}))
};
}
app.get('/api/state', asyncHandler(async (req, res) => {
res.json(await getState());
}));
app.post('/api/reset', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
await Product.create({
sku,
name: 'Workshop Mug',
price: 20,
stock: 3
});
res.json({ message: 'Lab reset', state: await getState() });
}));
app.post('/api/cleanup', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
res.json({ message: 'Lab data deleted', state: await getState() });
}));
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Unexpected server error' });
});
async function start() {
const uri = process.env.MONGODB_URI;
if (!uri || uri === 'your-mongodb-atlas-connection-string-here') {
throw new Error('Replace the MONGODB_URI placeholder in .env');
}
await mongoose.connect(uri, { dbName: 'mongoose_advanced_lab' });
await Product.createCollection();
await Order.createCollection();
console.log('Connected to MongoDB Atlas');
app.listen(port, () => {
console.log(`Shop lab running at http://localhost:${port}`);
});
}
start().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
What Should Match in server.js?
The state and reset routes appear before the cleanup route. The startup function creates both collections after connecting to the existing Atlas database.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Advanced Mongoose Shop Lab</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
background: #10131a;
color: #f3f5f7;
}
body {
max-width: 1000px;
margin: 0 auto;
padding: 32px 20px;
}
.panel {
background: #1b2230;
border: 1px solid #34405a;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
button {
border: 0;
border-radius: 8px;
padding: 10px 14px;
background: #67d8a4;
color: #102016;
font-weight: 700;
cursor: pointer;
}
button.danger {
background: #ff8d8d;
color: #2d0c0c;
}
pre {
min-height: 100px;
overflow: auto;
background: #0b0e14;
border-radius: 8px;
padding: 14px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>Advanced Mongoose Shop Lab</h1>
<p>Run each action in order and watch inventory, orders, rollback, virtuals, and aggregation results.</p>
<section class="panel">
<h2>Lab actions</h2>
<div class="buttons">
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
</div>
</section>
<section class="panel">
<h2>Current database state</h2>
<pre id="state">Loading...</pre>
</section>
<section class="panel">
<h2>Latest response</h2>
<pre id="output">Choose an action.</pre>
</section>
<script>
const state = document.querySelector('#state');
const output = document.querySelector('#output');
async function refreshState() {
const response = await fetch('/api/state');
const data = await response.json();
state.textContent = JSON.stringify(data, null, 2);
}
async function runAction(path, body) {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
};
if (body !== undefined) {
options.body = JSON.stringify(body);
}
const response = await fetch(path, options);
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
await refreshState();
}
refreshState().catch(error => {
output.textContent = error.message;
});
</script>
</body>
</html>
What Should Match in index.html?
The page contains both action buttons and both output panels. The script finishes with an initial call to refreshState().
Your dashboard now has reusable order behavior and two subtypes backed by one collection. Next, you will reproduce the partial inventory update that a transaction must prevent.
Reproduce the Partial Write
Your advanced Mongoose models now represent products plus two order subtypes. The dashboard can reset the lab to stock 3 with zero orders.
Checkout changes inventory before it saves an order. This step deliberately runs those writes separately so a failed order leaves a visible mismatch.
In this step, get ready to:
- Add a naive Express route that changes inventory before order validation.
- Connect the Broken Checkout button to the new route.
- Run the failing checkout to expose the partial write.
Add the naive checkout route
An Express route can finish one database write before the next write starts. Here, findOneAndUpdate() reduces stock before DeliveryOrder.create() validates the delivery fields.
- Return to Visual Studio Code.
- Select server.js in the Explorer sidebar.
- Locate the app.post('/api/cleanup' route.
- Add the naive checkout route below the cleanup route by pasting this code:
app.post('/api/checkout-naive', asyncHandler(async (req, res) => {
const product = await Product.findOneAndUpdate(
{ sku, stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after' }
);
if (!product) {
return res.status(409).json({ message: 'Out of stock' });
}
try {
await DeliveryOrder.create({
customerName: 'Ada Learner',
customerEmail: 'ADA@EXAMPLE.COM',
productSku: sku,
quantity: 1,
unitPrice: product.price
});
} catch (error) {
return res.status(409).json({
message: 'Order failed after stock changed',
error: error.message,
state: await getState()
});
}
return res.status(500).json({ message: 'The designed failure did not occur' });
}));
Why Does This Route Create a Partial Write?
- The findOneAndUpdate() call finds the product when at least one item remains.
- The $inc update immediately reduces stock by one.
- The DeliveryOrder.create() call omits the required deliveryAddress field.
- The validation failure enters the catch block after the inventory update has finished.
- The route returns status 409 with the refreshed database state.
- Save server.js.
- Stop the earlier server by pressing Ctrl+C if it is still running.
- Start the updated server by running this command:
npm start
What Should You See?
A successful MongoDB Atlas connection prints Connected to MongoDB Atlas. The next line shows Shop lab running at http://localhost:3000.
Does the Server Stop During Startup?
- Compare the braces around app.post('/api/checkout-naive' with the code above.
- Confirm that the new route appears before app.use((error, req, res, next) => {.
Ask for help with the exact terminal output: Help me find the syntax problem in my checkout-naive route.
Connect the Broken Checkout button
The existing runAction() function already sends POST requests. It also refreshes the state panel after each response.
- Select public/index.html in the Explorer sidebar.
- Locate these two buttons inside the buttons container:
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
Where Does the New Button Go?
These existing buttons mark the exact insertion point. The new action belongs between the reset control and the cleanup control.
- Replace that two-line block with this three-line block:
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/checkout-naive')">Broken Checkout</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
How Does the Button Work?
- The onclick handler passes /api/checkout-naive to the existing runAction() function.
- The danger class visually identifies the action as intentionally destructive.
- Save public/index.html.
- Return to the local dashboard in Microsoft Edge.
- Refresh the dashboard.
You should see Broken Checkout between Reset Lab and Delete Lab Data.
Good progress: the dashboard now has a dedicated control for the failing checkout.
Do You Still See Only Two Buttons?
- Confirm that public/index.html is saved.
- Refresh the local dashboard again.
- Confirm that the server terminal is still running.
Ask for help checking the button markup: Help me find why the Broken Checkout button is missing.
✔️ Awesome, I've got everything!
- Confirm that server.js is saved.
- Confirm that public/index.html is saved.
ⓧ I'd like to double check the full code
- Compare your two edited files with these cumulative versions.
const express = require('express');
const mongoose = require('mongoose');
const {
Product,
Order,
DeliveryOrder,
PickupOrder
} = require('./models');
const app = express();
const port = 3000;
const sku = 'MUG-001';
app.use(express.json());
app.use(express.static('public'));
function asyncHandler(handler) {
return function wrappedHandler(req, res, next) {
Promise.resolve(handler(req, res, next)).catch(next);
};
}
async function getState() {
const product = await Product.findOne({ sku });
const orders = await Order.find({}).sort({ createdAt: 1 });
return {
product: product ? product.toJSON() : null,
orders: orders.map(order => ({
...order.toJSON(),
summary: order.summary()
}))
};
}
app.get('/api/state', asyncHandler(async (req, res) => {
res.json(await getState());
}));
app.post('/api/reset', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
await Product.create({
sku,
name: 'Workshop Mug',
price: 20,
stock: 3
});
res.json({ message: 'Lab reset', state: await getState() });
}));
app.post('/api/cleanup', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
res.json({ message: 'Lab data deleted', state: await getState() });
}));
app.post('/api/checkout-naive', asyncHandler(async (req, res) => {
const product = await Product.findOneAndUpdate(
{ sku, stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after' }
);
if (!product) {
return res.status(409).json({ message: 'Out of stock' });
}
try {
await DeliveryOrder.create({
customerName: 'Ada Learner',
customerEmail: 'ADA@EXAMPLE.COM',
productSku: sku,
quantity: 1,
unitPrice: product.price
});
} catch (error) {
return res.status(409).json({
message: 'Order failed after stock changed',
error: error.message,
state: await getState()
});
}
return res.status(500).json({ message: 'The designed failure did not occur' });
}));
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Unexpected server error' });
});
async function start() {
const uri = process.env.MONGODB_URI;
if (!uri || uri === 'your-mongodb-atlas-connection-string-here') {
throw new Error('Replace the MONGODB_URI placeholder in .env');
}
await mongoose.connect(uri, { dbName: 'mongoose_advanced_lab' });
await Product.createCollection();
await Order.createCollection();
console.log('Connected to MongoDB Atlas');
app.listen(port, () => {
console.log(`Shop lab running at http://localhost:${port}`);
});
}
start().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Advanced Mongoose Shop Lab</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
background: #10131a;
color: #f3f5f7;
}
body {
max-width: 1000px;
margin: 0 auto;
padding: 32px 20px;
}
.panel {
background: #1b2230;
border: 1px solid #34405a;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
button {
border: 0;
border-radius: 8px;
padding: 10px 14px;
background: #67d8a4;
color: #102016;
font-weight: 700;
cursor: pointer;
}
button.danger {
background: #ff8d8d;
color: #2d0c0c;
}
pre {
min-height: 100px;
overflow: auto;
background: #0b0e14;
border-radius: 8px;
padding: 14px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>Advanced Mongoose Shop Lab</h1>
<p>Run each action in order and watch inventory, orders, rollback, virtuals, and aggregation results.</p>
<section class="panel">
<h2>Lab actions</h2>
<div class="buttons">
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/checkout-naive')">Broken Checkout</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
</div>
</section>
<section class="panel">
<h2>Current database state</h2>
<pre id="state">Loading...</pre>
</section>
<section class="panel">
<h2>Latest response</h2>
<pre id="output">Choose an action.</pre>
</section>
<script>
const state = document.querySelector('#state');
const output = document.querySelector('#output');
async function refreshState() {
const response = await fetch('/api/state');
const data = await response.json();
state.textContent = JSON.stringify(data, null, 2);
}
async function runAction(path, body) {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
};
if (body !== undefined) {
options.body = JSON.stringify(body);
}
const response = await fetch(path, options);
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
await refreshState();
}
refreshState().catch(error => {
output.textContent = error.message;
});
</script>
</body>
</html>
Watch the partial write happen
The route plus button are ready. The important evidence comes from comparing inventory with saved orders after validation rejects the checkout.
- Click Reset Lab.
The Current database state panel should show stock 3 with an empty orders array.
Before you run the broken checkout, do you expect both database changes to fail together?
- Click Broken Checkout.
The Latest response panel reports Order failed after stock changed. The error mentions the missing deliveryAddress field.
- Compare the product stock with the orders array in the Current database state panel.
You should see "stock": 2 with "orders": []. The order failed. The inventory update persisted.
This Is the Partial Write
The checkout completed one database write before validation rejected the next write. Your product plus order records now disagree about whether a sale happened.
You have now caught the exact consistency bug that unsafe checkout code can create. Next, you will place both writes inside a transaction so a failed order also restores the inventory.
Make Checkout Atomic
The broken checkout proved the risk of splitting one checkout across independent writes. Inventory fell even though the invalid order never reached the database.
A Mongoose transaction groups the stock update with the order creation. One shared session tells MongoDB which writes must commit or roll back together.
In this step, get ready to:
- Add a session-backed checkout route for delivery or pickup orders.
- Connect three safe checkout controls to the route.
- Verify failed work rolls back while valid orders commit.
Add the transaction-backed route
A transaction only protects operations that receive its session. The route passes the same session to the inventory update and order creation.
This route spans two connected code blocks. Paste both halves before saving so every open scope is closed.
- Switch back to server.js in Visual Studio Code.
- Find the line that starts with app.use((error, req, res, next) => {.
- Insert the first half of the safe checkout route immediately above that line by pasting this code:
app.post('/api/checkout-safe', asyncHandler(async (req, res) => {
const { kind, forceFailure = false } = req.body || {};
if (!['delivery', 'pickup'].includes(kind)) {
return res.status(400).json({ message: 'Choose delivery or pickup' });
}
let createdOrder;
try {
await mongoose.connection.transaction(async session => {
const product = await Product.findOneAndUpdate(
{ sku, stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after', session }
);
if (!product) {
throw new Error('Out of stock');
}
const OrderModel = kind === 'delivery' ? DeliveryOrder : PickupOrder;
const orderData = {
customerName: 'Ada Learner',
customerEmail: 'ADA@EXAMPLE.COM',
productSku: sku,
quantity: 1,
unitPrice: product.price
};
What does the route set up?
- The request body supplies kind plus the optional forceFailure switch.
- The kind check accepts only delivery or pickup.
- The transaction callback receives the shared session.
- The stock update includes that session in its options.
- The selected discriminator controls which subtype validates the order.
- Complete the same route by pasting this code directly below the first half:
if (!forceFailure && kind === 'delivery') {
orderData.deliveryAddress = '42 Learning Lane';
}
if (!forceFailure && kind === 'pickup') {
orderData.pickupCode = 'DESK-7';
}
[createdOrder] = await OrderModel.create([orderData], { session });
});
return res.status(201).json({
message: `${kind} checkout committed`,
order: createdOrder.toJSON(),
summary: createdOrder.summary(),
state: await getState()
});
} catch (error) {
return res.status(409).json({
message: 'Transaction rolled back',
error: error.message,
state: await getState()
});
}
}));
How does the transaction finish?
- A valid delivery order receives deliveryAddress.
- A valid pickup order receives pickupCode.
- The array form of OrderModel.create() allows the route to pass { session }.
- A successful callback commits both writes.
- A validation error reaches the catch block after the transaction aborts.
- Save server.js.
- Return to the integrated terminal from earlier.
- Stop the running server by pressing Ctrl+C.
Before you restart the server, do you expect the new route to compile successfully?
- Restart the shop server by running this command:
npm start
What does this check prove?
A successful restart confirms that both route fragments form valid JavaScript. The server reconnects before it begins accepting dashboard requests.
You should see Connected to MongoDB Atlas followed by Shop lab running at http://localhost:3000.
Server not restarting?
- Check that the second route fragment sits directly below the first fragment.
- Confirm the safe route appears above the existing error middleware.
- Compare the closing braces around the transaction callback with the complete reference below.
Help me find the problem in my safe checkout route.
Connect the safe checkout controls
The browser needs one control for the rollback demonstration plus two controls for successful orders. Each control sends a different request body through the existing runAction() helper.
- Switch back to public/index.html in Visual Studio Code.
- Find this button block inside the element with class="buttons":
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/checkout-naive')">Broken Checkout</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
The current control row
The dashboard currently exposes the reset action and the deliberately broken checkout. The cleanup control remains at the end of the row.
- Replace that button block with this expanded version:
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/checkout-naive')">Broken Checkout</button>
<button onclick="safeCheckout('delivery', true)">Safe Rollback</button>
<button onclick="safeCheckout('delivery', false)">Safe Delivery</button>
<button onclick="safeCheckout('pickup', false)">Safe Pickup</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
What changes here?
- Safe Rollback requests a delivery order while forcing its required field to stay missing.
- Safe Delivery requests a complete delivery order.
- Safe Pickup requests a complete pickup order.
- Find the closing brace of runAction().
- Add the safe checkout helper directly below runAction() by pasting this code:
async function safeCheckout(kind, forceFailure) {
await runAction('/api/checkout-safe', { kind, forceFailure });
}
What does this helper do?
The helper sends kind plus forceFailure to the safe route. The existing request helper displays the response before refreshing the database state.
- Save public/index.html.
- Return to the dashboard in Microsoft Edge.
- Refresh http://localhost:3000.
Your dashboard now shows Safe Rollback, Safe Delivery, and Safe Pickup. The three controls are connected to one checkout route.
Before you click Safe Rollback, do you expect the failed validation to leave stock at 2 or reduce it to 1?
- Click Safe Rollback.
You should see Transaction rolled back in the latest response. The current state stays at stock 2 with zero orders.
Did stock change?
- Confirm the stock update includes session in its options.
- Confirm OrderModel.create() receives the same { session } value.
- Confirm the Safe Rollback button passes true as its second argument.
Help me diagnose why Safe Rollback changed my inventory.
Commit valid delivery and pickup orders
The rollback proved that failed work leaves no partial state. Valid subtype data now lets the same transaction commit its stock update and order together.
Before you place both valid orders, what should happen to the remaining stock after two successful commits?
- Click Safe Delivery.
You should see stock 1 with one saved DeliveryOrder.
- Click Safe Pickup.
You should see stock 0 with two saved orders. One order has kind DeliveryOrder while the other has kind PickupOrder.
- Switch back to the integrated terminal from earlier.
You should see two lines beginning with [audit]. That is the full checkout path working from browser request to committed database state.
What proves the model behavior works?
- Both documents show ada@example.com because the validation hook normalized the original email.
- The delivery document shows total: 25 because its virtual includes the shipping fee.
- The pickup document shows total: 20 because it has no shipping fee.
- The summaries read DeliveryOrder: 1 x MUG-001 = $25.00 and PickupOrder: 1 x MUG-001 = $20.00.
- The discriminator key identifies both subtypes inside the shared orders collection.
✔️ Awesome, I've got everything!
Great work. Save both changed files before moving on.
ⓧ I'd like to double check the full code
Compare your two changed files with these cumulative versions. The other project files remain unchanged.
const express = require('express');
const mongoose = require('mongoose');
const {
Product,
Order,
DeliveryOrder,
PickupOrder
} = require('./models');
const app = express();
const port = 3000;
const sku = 'MUG-001';
app.use(express.json());
app.use(express.static('public'));
function asyncHandler(handler) {
return function wrappedHandler(req, res, next) {
Promise.resolve(handler(req, res, next)).catch(next);
};
}
async function getState() {
const product = await Product.findOne({ sku });
const orders = await Order.find({}).sort({ createdAt: 1 });
return {
product: product ? product.toJSON() : null,
orders: orders.map(order => ({
...order.toJSON(),
summary: order.summary()
}))
};
}
app.get('/api/state', asyncHandler(async (req, res) => {
res.json(await getState());
}));
app.post('/api/reset', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
await Product.create({
sku,
name: 'Workshop Mug',
price: 20,
stock: 3
});
res.json({ message: 'Lab reset', state: await getState() });
}));
app.post('/api/cleanup', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
res.json({ message: 'Lab data deleted', state: await getState() });
}));
app.post('/api/checkout-naive', asyncHandler(async (req, res) => {
const product = await Product.findOneAndUpdate(
{ sku, stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after' }
);
if (!product) {
return res.status(409).json({ message: 'Out of stock' });
}
try {
await DeliveryOrder.create({
customerName: 'Ada Learner',
customerEmail: 'ADA@EXAMPLE.COM',
productSku: sku,
quantity: 1,
unitPrice: product.price
});
} catch (error) {
return res.status(409).json({
message: 'Order failed after stock changed',
error: error.message,
state: await getState()
});
}
return res.status(500).json({ message: 'The designed failure did not occur' });
}));
app.post('/api/checkout-safe', asyncHandler(async (req, res) => {
const { kind, forceFailure = false } = req.body || {};
if (!['delivery', 'pickup'].includes(kind)) {
return res.status(400).json({ message: 'Choose delivery or pickup' });
}
let createdOrder;
try {
await mongoose.connection.transaction(async session => {
const product = await Product.findOneAndUpdate(
{ sku, stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after', session }
);
if (!product) {
throw new Error('Out of stock');
}
const OrderModel = kind === 'delivery' ? DeliveryOrder : PickupOrder;
const orderData = {
customerName: 'Ada Learner',
customerEmail: 'ADA@EXAMPLE.COM',
productSku: sku,
quantity: 1,
unitPrice: product.price
};
if (!forceFailure && kind === 'delivery') {
orderData.deliveryAddress = '42 Learning Lane';
}
if (!forceFailure && kind === 'pickup') {
orderData.pickupCode = 'DESK-7';
}
[createdOrder] = await OrderModel.create([orderData], { session });
});
return res.status(201).json({
message: `${kind} checkout committed`,
order: createdOrder.toJSON(),
summary: createdOrder.summary(),
state: await getState()
});
} catch (error) {
return res.status(409).json({
message: 'Transaction rolled back',
error: error.message,
state: await getState()
});
}
}));
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Unexpected server error' });
});
async function start() {
const uri = process.env.MONGODB_URI;
if (!uri || uri === 'your-mongodb-atlas-connection-string-here') {
throw new Error('Replace the MONGODB_URI placeholder in .env');
}
await mongoose.connect(uri, { dbName: 'mongoose_advanced_lab' });
await Product.createCollection();
await Order.createCollection();
console.log('Connected to MongoDB Atlas');
app.listen(port, () => {
console.log(`Shop lab running at http://localhost:${port}`);
});
}
start().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
What to compare in server.js
Check that the safe route passes the same session to both database operations. Confirm the route still appears before the existing error middleware.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Advanced Mongoose Shop Lab</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
background: #10131a;
color: #f3f5f7;
}
body {
max-width: 1000px;
margin: 0 auto;
padding: 32px 20px;
}
.panel {
background: #1b2230;
border: 1px solid #34405a;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
button {
border: 0;
border-radius: 8px;
padding: 10px 14px;
background: #67d8a4;
color: #102016;
font-weight: 700;
cursor: pointer;
}
button.danger {
background: #ff8d8d;
color: #2d0c0c;
}
pre {
min-height: 100px;
overflow: auto;
background: #0b0e14;
border-radius: 8px;
padding: 14px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>Advanced Mongoose Shop Lab</h1>
<p>Run each action in order and watch inventory, orders, rollback, virtuals, and aggregation results.</p>
<section class="panel">
<h2>Lab actions</h2>
<div class="buttons">
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/checkout-naive')">Broken Checkout</button>
<button onclick="safeCheckout('delivery', true)">Safe Rollback</button>
<button onclick="safeCheckout('delivery', false)">Safe Delivery</button>
<button onclick="safeCheckout('pickup', false)">Safe Pickup</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
</div>
</section>
<section class="panel">
<h2>Current database state</h2>
<pre id="state">Loading...</pre>
</section>
<section class="panel">
<h2>Latest response</h2>
<pre id="output">Choose an action.</pre>
</section>
<script>
const state = document.querySelector('#state');
const output = document.querySelector('#output');
async function refreshState() {
const response = await fetch('/api/state');
const data = await response.json();
state.textContent = JSON.stringify(data, null, 2);
}
async function runAction(path, body) {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
};
if (body !== undefined) {
options.body = JSON.stringify(body);
}
const response = await fetch(path, options);
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
await refreshState();
}
async function safeCheckout(kind, forceFailure) {
await runAction('/api/checkout-safe', { kind, forceFailure });
}
refreshState().catch(error => {
output.textContent = error.message;
});
</script>
</body>
</html>
What to compare in index.html
Check that all three safe controls call safeCheckout() with the intended values. Confirm the helper sends both request fields through runAction().
Checkout now protects inventory and order creation as one atomic operation. Next up, you will turn the committed orders into a sales report grouped by order type.
Build the Sales Report
Your transaction-safe checkout now commits delivery orders as one unit. It does the same for pickup orders.
The dashboard can show each hydrated order. Sales reporting needs a collection-wide view. A Mongoose aggregation pipeline can group the stored orders by discriminator type. You will expose that reusable report through an Express route.
In this step, get ready to:
- Define a model static that groups paid orders by discriminator type.
- Expose the aggregation result through a report route.
- Render the sales report from the dashboard.
Define the report static
A model static keeps collection-wide reporting logic attached to the model it describes. The pipeline filters paid orders first. It groups the remaining documents next. Its final stage shapes the result for the dashboard.
- Switch back to models.js in Visual Studio Code.
- Find the line that compiles the Product model.
- Add the report static directly above that line by copying this code:
orderSchema.statics.salesByType = function salesByType() {
return this.aggregate([
{ $match: { status: 'paid' } },
{
$group: {
_id: '$kind',
orderCount: { $sum: 1 },
unitsSold: { $sum: '$quantity' }
}
},
{
$project: {
_id: 0,
orderType: '$_id',
orderCount: 1,
unitsSold: 1
}
}
]);
};
What Does This Pipeline Do?
- The $match stage keeps orders whose status is paid.
- The $group stage creates one result for each value stored in $kind.
- The first $sum counts the orders in each group.
- The second $sum totals the stored quantities in each group.
- The $project stage renames the group key to orderType.
- Setting _id to 0 removes the internal grouping field from the report.
- Save models.js.
- Return to the terminal from earlier.
- Press Ctrl+C to stop the running server.
- Restart the server with the updated model by running:
npm start
You should see Connected to MongoDB Atlas again. This confirms Mongoose compiled the schema with the new static.
Server Not Reconnecting?
Check that salesByType appears above the lines that compile Product and Order. A schema static must exist before its model is compiled.
Compare every brace in the pipeline with the code block above. A missing brace prevents models.js from loading.
Ask for help with the startup output you see.
Expose the report route
The static can now build the report inside the model layer. A small HTTP route gives the browser a way to request that result.
- Switch back to server.js in Visual Studio Code.
- Find the error-handling middleware that starts with app.use((error, req, res, next) => {.
- Add the report route directly above that middleware by copying this code:
app.get('/api/report', asyncHandler(async (req, res) => {
res.json(await Order.salesByType());
}));
What Does This Route Do?
- The /api/report route responds to a browser GET request.
- The route calls Order.salesByType() to run the pipeline against the shared orders collection.
- The response contains plain aggregation objects. Hydrated document features such as total and summary() belong to the state response instead.
- Save server.js.
- Return to the terminal.
- Press Ctrl+C to stop the current server.
- Restart the server with the new route by running:
npm start
- Return to Microsoft Edge.
- Press Ctrl+L to focus the address bar.
- Enter http://localhost:3000/api/report.
- Press Enter to request the report.
You should see one object for DeliveryOrder. You should also see one object for PickupOrder.
Each object should contain an orderCount of 1. Each object should contain a unitsSold value of 1.
Report Route Not Returning Two Rows?
Return to the dashboard if the report is empty. Run Reset Lab first. Run Safe Delivery next. Run Safe Pickup last.
Check that the route calls Order.salesByType(). Calling the static on a discriminator would report through a narrower model.
Ask for help with the JSON response you received.
Add the Sales Report button
The API report works directly in the browser. The final change brings that result into the dashboard's Latest response panel.
- Switch back to public/index.html in Visual Studio Code.
- Find these two button lines:
<button onclick="safeCheckout('pickup', false)">Safe Pickup</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
- Replace those two lines with this three-button version:
<button onclick="safeCheckout('pickup', false)">Safe Pickup</button>
<button onclick="showReport()">Sales Report</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
- Find the closing brace of safeCheckout().
- Add the report function directly below that closing brace by copying this code:
async function showReport() {
const response = await fetch('/api/report');
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
}
How Does the Dashboard Load the Report?
- The button calls showReport() when you click it.
- The function requests /api/report with the browser's fetch API.
- The response is converted from JSON into a JavaScript value.
- The formatted result replaces the content of the output element.
- Save public/index.html.
- Click the browser's Back button to return to the dashboard.
- Press Ctrl+R to load the updated dashboard.
Before you click the new button, which order types do you expect the report to group?
- Click Sales Report in the dashboard.
You should see a DeliveryOrder row in Latest response. You should also see a PickupOrder row.
Each row shows orderCount as 1. Each row shows unitsSold as 1.
Why Are the State and Report Different?
The state panel reads hydrated Mongoose documents. Those documents expose the total virtual plus the result of summary().
The report contains plain aggregation results built from stored fields. This makes the output focused on totals across the collection.
Sales Report Button Not Working?
Check that the button calls showReport() with matching capitalization. Check that the function requests /api/report.
Refresh the dashboard after saving public/index.html. The browser can keep the previous page markup until you reload it.
Ask for help with the dashboard behavior you see.
✔️ Awesome, I've got everything!
Great work. Your dashboard now turns saved delivery and pickup orders into a grouped sales report.
ⓧ I'd like to double check the full code
The three files changed in this step should match the versions below.
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
sku: { type: String, required: true, unique: true },
name: { type: String, required: true },
price: { type: Number, required: true, min: 0 },
stock: { type: Number, required: true, min: 0 }
});
const orderSchema = new mongoose.Schema(
{
customerName: { type: String, required: true },
customerEmail: { type: String, required: true },
productSku: { type: String, required: true },
quantity: { type: Number, required: true, min: 1 },
unitPrice: { type: Number, required: true, min: 0 },
status: { type: String, enum: ['paid'], default: 'paid' }
},
{
discriminatorKey: 'kind',
collection: 'orders',
timestamps: true,
toJSON: { virtuals: true },
toObject: { virtuals: true }
}
);
orderSchema.pre('validate', function normalizeCustomerEmail() {
if (this.customerEmail) {
this.customerEmail = this.customerEmail.trim().toLowerCase();
}
});
orderSchema.post('save', function logSavedOrder(doc) {
console.log(`[audit] saved ${doc.kind} ${doc._id}`);
});
orderSchema.virtual('total').get(function calculateTotal() {
return this.quantity * this.unitPrice + (this.shippingFee || 0);
});
orderSchema.methods.summary = function summary() {
return `${this.kind}: ${this.quantity} x ${this.productSku} = $${this.total.toFixed(2)}`;
};
orderSchema.statics.salesByType = function salesByType() {
return this.aggregate([
{ $match: { status: 'paid' } },
{
$group: {
_id: '$kind',
orderCount: { $sum: 1 },
unitsSold: { $sum: '$quantity' }
}
},
{
$project: {
_id: 0,
orderType: '$_id',
orderCount: 1,
unitsSold: 1
}
}
]);
};
const Product = mongoose.model('Product', productSchema);
const Order = mongoose.model('Order', orderSchema);
const DeliveryOrder = Order.discriminator(
'DeliveryOrder',
new mongoose.Schema({
deliveryAddress: { type: String, required: true },
shippingFee: { type: Number, default: 5, min: 0 }
})
);
const PickupOrder = Order.discriminator(
'PickupOrder',
new mongoose.Schema({
pickupCode: { type: String, required: true }
})
);
module.exports = {
Product,
Order,
DeliveryOrder,
PickupOrder
};
const express = require('express');
const mongoose = require('mongoose');
const {
Product,
Order,
DeliveryOrder,
PickupOrder
} = require('./models');
const app = express();
const port = 3000;
const sku = 'MUG-001';
app.use(express.json());
app.use(express.static('public'));
function asyncHandler(handler) {
return function wrappedHandler(req, res, next) {
Promise.resolve(handler(req, res, next)).catch(next);
};
}
async function getState() {
const product = await Product.findOne({ sku });
const orders = await Order.find({}).sort({ createdAt: 1 });
return {
product: product ? product.toJSON() : null,
orders: orders.map(order => ({
...order.toJSON(),
summary: order.summary()
}))
};
}
app.get('/api/state', asyncHandler(async (req, res) => {
res.json(await getState());
}));
app.post('/api/reset', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
await Product.create({
sku,
name: 'Workshop Mug',
price: 20,
stock: 3
});
res.json({ message: 'Lab reset', state: await getState() });
}));
app.post('/api/cleanup', asyncHandler(async (req, res) => {
await Order.deleteMany({});
await Product.deleteMany({});
res.json({ message: 'Lab data deleted', state: await getState() });
}));
app.post('/api/checkout-naive', asyncHandler(async (req, res) => {
const product = await Product.findOneAndUpdate(
{ sku, stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after' }
);
if (!product) {
return res.status(409).json({ message: 'Out of stock' });
}
try {
await DeliveryOrder.create({
customerName: 'Ada Learner',
customerEmail: 'ADA@EXAMPLE.COM',
productSku: sku,
quantity: 1,
unitPrice: product.price
});
} catch (error) {
return res.status(409).json({
message: 'Order failed after stock changed',
error: error.message,
state: await getState()
});
}
return res.status(500).json({ message: 'The designed failure did not occur' });
}));
app.post('/api/checkout-safe', asyncHandler(async (req, res) => {
const { kind, forceFailure = false } = req.body || {};
if (!['delivery', 'pickup'].includes(kind)) {
return res.status(400).json({ message: 'Choose delivery or pickup' });
}
let createdOrder;
try {
await mongoose.connection.transaction(async session => {
const product = await Product.findOneAndUpdate(
{ sku, stock: { $gte: 1 } },
{ $inc: { stock: -1 } },
{ returnDocument: 'after', session }
);
if (!product) {
throw new Error('Out of stock');
}
const OrderModel = kind === 'delivery' ? DeliveryOrder : PickupOrder;
const orderData = {
customerName: 'Ada Learner',
customerEmail: 'ADA@EXAMPLE.COM',
productSku: sku,
quantity: 1,
unitPrice: product.price
};
if (!forceFailure && kind === 'delivery') {
orderData.deliveryAddress = '42 Learning Lane';
}
if (!forceFailure && kind === 'pickup') {
orderData.pickupCode = 'DESK-7';
}
[createdOrder] = await OrderModel.create([orderData], { session });
});
return res.status(201).json({
message: `${kind} checkout committed`,
order: createdOrder.toJSON(),
summary: createdOrder.summary(),
state: await getState()
});
} catch (error) {
return res.status(409).json({
message: 'Transaction rolled back',
error: error.message,
state: await getState()
});
}
}));
app.get('/api/report', asyncHandler(async (req, res) => {
res.json(await Order.salesByType());
}));
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: 'Unexpected server error' });
});
async function start() {
const uri = process.env.MONGODB_URI;
if (!uri || uri === 'your-mongodb-atlas-connection-string-here') {
throw new Error('Replace the MONGODB_URI placeholder in .env');
}
await mongoose.connect(uri, { dbName: 'mongoose_advanced_lab' });
await Product.createCollection();
await Order.createCollection();
console.log('Connected to MongoDB Atlas');
app.listen(port, () => {
console.log(`Shop lab running at http://localhost:${port}`);
});
}
start().catch(error => {
console.error(error.message);
process.exitCode = 1;
});
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Advanced Mongoose Shop Lab</title>
<style>
:root {
color-scheme: dark;
font-family: system-ui, sans-serif;
background: #10131a;
color: #f3f5f7;
}
body {
max-width: 1000px;
margin: 0 auto;
padding: 32px 20px;
}
.panel {
background: #1b2230;
border: 1px solid #34405a;
border-radius: 12px;
padding: 18px;
margin-bottom: 18px;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
button {
border: 0;
border-radius: 8px;
padding: 10px 14px;
background: #67d8a4;
color: #102016;
font-weight: 700;
cursor: pointer;
}
button.danger {
background: #ff8d8d;
color: #2d0c0c;
}
pre {
min-height: 100px;
overflow: auto;
background: #0b0e14;
border-radius: 8px;
padding: 14px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>Advanced Mongoose Shop Lab</h1>
<p>Run each action in order and watch inventory, orders, rollback, virtuals, and aggregation results.</p>
<section class="panel">
<h2>Lab actions</h2>
<div class="buttons">
<button onclick="runAction('/api/reset')">Reset Lab</button>
<button class="danger" onclick="runAction('/api/checkout-naive')">Broken Checkout</button>
<button onclick="safeCheckout('delivery', true)">Safe Rollback</button>
<button onclick="safeCheckout('delivery', false)">Safe Delivery</button>
<button onclick="safeCheckout('pickup', false)">Safe Pickup</button>
<button onclick="showReport()">Sales Report</button>
<button class="danger" onclick="runAction('/api/cleanup')">Delete Lab Data</button>
</div>
</section>
<section class="panel">
<h2>Current database state</h2>
<pre id="state">Loading...</pre>
</section>
<section class="panel">
<h2>Latest response</h2>
<pre id="output">Choose an action.</pre>
</section>
<script>
const state = document.querySelector('#state');
const output = document.querySelector('#output');
async function refreshState() {
const response = await fetch('/api/state');
const data = await response.json();
state.textContent = JSON.stringify(data, null, 2);
}
async function runAction(path, body) {
const options = {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
};
if (body !== undefined) {
options.body = JSON.stringify(body);
}
const response = await fetch(path, options);
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
await refreshState();
}
async function safeCheckout(kind, forceFailure) {
await runAction('/api/checkout-safe', { kind, forceFailure });
}
async function showReport() {
const response = await fetch('/api/report');
const data = await response.json();
output.textContent = JSON.stringify(data, null, 2);
}
refreshState().catch(error => {
output.textContent = error.message;
});
</script>
</body>
</html>
You built the final reporting layer. Your dashboard now proves atomic checkout behavior at the document level. It also summarizes committed sales across the entire orders collection.
Secret mission
Add Gift Orders Without a New Collection
Your shop currently supports delivery and pickup orders through one shared model. Add a GiftOrder subtype that reuses the existing transaction workflow and appears in the sales report automatically.
Clean Up Your Resources
Clean Up Your Resources
Choose whether to keep the dashboard active, pause its local server, or delete its lab data and files. The local server and project files have no ongoing cost.
Cost warning
A MongoDB Atlas Free cluster costs $0/hour.
An existing paid cluster continues its normal tier charges. The options below do not alter that cluster's billing.
Resources you used:
- The local Node.js server running from the mongoose-advanced-shop folder on your Desktop.
- The lab documents in the products collection inside the mongoose_advanced_lab database.
- The lab documents in the orders collection inside the same database.
- The mongoose-advanced-shop folder on your Desktop with its source files.
- The node_modules dependencies and private .env file inside the project folder.
Keep everything running
No action is needed. Choose this option if you want to keep testing transactions or extending the dashboard.
- Leave the local dashboard server running.
- Keep the lab documents in the mongoose_advanced_lab database.
- Keep the mongoose-advanced-shop folder on your Desktop.
- Keep .env private because it contains your Atlas connection string.
Pause - I'll come back to this later
Shut down the local server to free up memory. Your Atlas data and project files remain available.
- Return to the PowerShell terminal from earlier.
- Press Ctrl+C to stop the local server.
- Confirm the terminal returns to a command prompt.
- Return to the mongoose-advanced-shop folder in the same terminal when you are ready to resume.
- Restart the dashboard by running this command:
npm start
What does this command do?
The npm command runs the start script from package.json.
That script loads .env before starting server.js.
- Confirm the terminal shows Connected to MongoDB Atlas.
- Confirm the terminal shows Shop lab running at http://localhost:3000.
- Return to the browser dashboard from earlier.
- Refresh the dashboard.
- Confirm the page shows Advanced Mongoose Shop Lab.
Your dashboard is ready again. Its Atlas data remains available.
Dashboard does not restart?
- Check that PowerShell is inside the mongoose-advanced-shop folder containing package.json.
- Confirm that .env still contains the MONGODB_URI entry from earlier.
Ask for help diagnosing why the shop dashboard cannot restart.
Delete - I don't want to use this again
This cleanup removes only the lab documents and local project folder. Your existing Atlas cluster remains intact.
- Return to the shop dashboard in your browser from earlier.
- Click Delete Lab Data in the Lab actions panel.
- Confirm Latest response shows Lab data deleted.
- Confirm Current database state shows product as null.
- Confirm Current database state shows an empty orders array.
That is the database cleanup finished. Your existing Atlas cluster remains available for other work.
- Return to the PowerShell terminal from earlier.
- Press Ctrl+C to stop the local server.
- Confirm the terminal returns to a command prompt.
Removing the project folder deletes every source file. It also deletes the private .env file.
- Return to your Windows Desktop.
- Right-click the mongoose-advanced-shop folder.
- Choose the folder removal option from the context menu.
- Confirm the mongoose-advanced-shop folder no longer appears on your Desktop.
That is a clean finish: the lab data is gone from Atlas. The local project folder is gone too.
Nice Work!
Nice Work!
You did it! Your shop dashboard now protects checkout data from partial writes.
What you learned:
- Built a full stack dashboard that connects the browser to Express, Mongoose, and MongoDB Atlas. Its Sales Report runs an aggregation pipeline with $match, $group, and $project to group paid orders by type.
- Created model-owned behavior with pre and post hooks that normalize emails and print audit lines. Added a serialized total virtual, a summary() instance method, a salesByType() static, and discriminators that share one orders collection.
- Reproduced a partial-write bug where inventory changed without an order. Repaired checkout with an ACID transaction that uses one session to commit both writes or roll them back.
- Completed the optional Secret Mission by adding the GiftOrder discriminator to the existing orders collection. The same transaction and report now handle gift orders without duplicated checkout logic.
Ready to quiz yourself?