1. The Problem
Build an inventory system that tracks how many of each item are in stock. You can restock (add quantity), sell (remove quantity, but never below zero), and list every item running low on stock.
Why this project? Encapsulating state in a class, guarding operations with validation (you cannot sell what you do not have), and filtering a collection by a condition โ the backbone of any business system.
2. How to Think About This
Hold the stock as an object mapping item name to quantity. restock adds to the current count (defaulting to 0 if the item is new). sell must first check there is enough โ throw an error rather than let stock go negative, because a silent bad state is worse than a loud failure. lowStock filters the entries down to those below a threshold.
3. The Build
export class Inventory {
constructor() {
this.items = {}; // item name -> quantity
}
// Add stock (creates the item if it is new).
restock(item, quantity) {
this.items[item] = (this.items[item] || 0) + quantity;
}
// Remove stock โ refuses to go below zero.
sell(item, quantity) {
if ((this.items[item] || 0) < quantity) {
throw new Error(`Not enough ${item} in stock`);
}
this.items[item] -= quantity;
}
// Items at or below the low-stock threshold.
lowStock(threshold = 5) {
return Object.entries(this.items)
.filter(([, qty]) => qty < threshold)
.map(([item]) => item);
}
}
// Demo
const inv = new Inventory();
inv.restock("apples", 10);
inv.sell("apples", 7);
console.log("Apples left:", inv.items.apples);
console.log("Low stock:", inv.lowStock());
4. Test & Prove
import { Inventory } from "./inventory.mjs";
let pass = 0, fail = 0;
function test(desc, fn) {
try { fn(); console.log(" \u2713", desc); pass++; }
catch (e) { console.log(" \u2717", desc, "\u2014", e.message); fail++; }
}
function assert(cond, msg) { if (!cond) throw new Error(msg || "assertion failed"); }
test("restock adds to a new item", () => {
const inv = new Inventory();
inv.restock("pens", 12);
assert(inv.items.pens === 12);
});
test("restock accumulates", () => {
const inv = new Inventory();
inv.restock("pens", 5);
inv.restock("pens", 3);
assert(inv.items.pens === 8);
});
test("sell reduces stock", () => {
const inv = new Inventory();
inv.restock("apples", 10);
inv.sell("apples", 7);
assert(inv.items.apples === 3);
});
test("selling more than in stock throws", () => {
const inv = new Inventory();
inv.restock("apples", 2);
let threw = false;
try { inv.sell("apples", 5); } catch { threw = true; }
assert(threw, "expected an error");
});
test("lowStock flags items under the threshold", () => {
const inv = new Inventory();
inv.restock("apples", 3);
inv.restock("bananas", 20);
assert(JSON.stringify(inv.lowStock()) === JSON.stringify(["apples"]));
});
console.log(`\n${pass} passed, ${fail} failed`);
5. The Interface
node inventory.mjs Apples left: 3 Low stock: [ 'apples' ] # Restock and sell as your stock moves; lowStock() flags what to reorder.
6. Run It
node inventory.mjs
node inventory.test.mjs๐ฏ Try this next
- Persist the inventory to a JSON file so it survives restarts
- Add a total inventory value using a per-item price
- Record a transaction log of every restock and sale with timestamps
- Add a reorder suggestion: how many to buy to reach a target level
What you practised
Encapsulating state in a class, guarding operations with validation and thrown errors, and filtering with Object.entries() + filter() + map().
Reference: Objects ยท Error Handling ยท Arrays