Building an e-commerce application from scratch is one of the best exercises in full-stack engineering. You're integrating authentication, a product catalog, a relational database, a shopping cart, order management, and an admin layer — all of which have to work together reliably. The Robotics E-Commerce project was my implementation of that full problem.
Architecture Overview
The application is a multi-tier web app: HTML/CSS/JavaScript frontend (no framework, which was intentional — I wanted to understand the fundamentals without abstraction), a Node.js + Express REST API backend, and a MySQL relational database. The 69% JavaScript / 16% CSS / 13% HTML split reflects a fairly rich frontend interaction layer.
- ›Frontend: vanilla JS with fetch-based API calls, localStorage for cart state, dynamic DOM updates
- ›Backend: Express REST API under /server — routes, middleware, controllers, and db layer
- ›Database: MySQL with normalized schema — users, products, orders, order_items, reviews
- ›Auth: JWT tokens for customers, ADMIN_PASSWORD env var for admin routes
- ›Dev setup: npm run db:init and db:seed for reproducible environments
JWT Authentication Design
JWT was the right choice for customer authentication here: stateless, easy to validate on every route, and simple to implement without a session store. When a customer logs in, the API signs a token with JWT_SECRET (from .env) and returns it to the client, which stores it in localStorage and sends it as an Authorization header on subsequent requests.
1// Auth middleware — applied to all protected routes
2const jwt = require('jsonwebtoken');
3
4function requireAuth(req, res, next) {
5 const header = req.headers.authorization;
6 if (!header || !header.startsWith('Bearer ')) {
7 return res.status(401).json({ error: 'Authentication required' });
8 }
9
10 const token = header.split(' ')[1];
11 try {
12 const payload = jwt.verify(token, process.env.JWT_SECRET);
13 req.user = payload; // { id, email, username }
14 next();
15 } catch {
16 return res.status(401).json({ error: 'Invalid or expired token' });
17 }
18}
19
20module.exports = { requireAuth };Passwords are hashed with bcryptjs before storage — bcrypt's cost factor default (10 rounds) provides strong resistance to brute-force attacks while keeping login response times acceptable. The admin interface uses a simpler approach: a request header value is compared against ADMIN_PASSWORD from the environment. This avoids the complexity of a separate admin account system for a project of this scope.
MySQL Schema and Atomic Checkout
The schema design was driven by two requirements: support all customer workflows (browse, review, cart, order) and ensure checkout is atomic — meaning inventory is decremented and the order is recorded in the same transaction, or neither happens.
1// Atomic checkout transaction
2async function placeOrder(userId, cartItems, connection) {
3 await connection.beginTransaction();
4 try {
5 // 1. Insert order record
6 const [orderResult] = await connection.execute(
7 'INSERT INTO orders (user_id, total, status) VALUES (?, ?, ?)',
8 [userId, calculateTotal(cartItems), 'pending']
9 );
10 const orderId = orderResult.insertId;
11
12 // 2. Insert order line items + decrement stock
13 for (const item of cartItems) {
14 await connection.execute(
15 'INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)',
16 [orderId, item.productId, item.quantity, item.price]
17 );
18 await connection.execute(
19 'UPDATE products SET stock = stock - ? WHERE id = ? AND stock >= ?',
20 [item.quantity, item.productId, item.quantity]
21 );
22 }
23
24 await connection.commit();
25 return orderId;
26 } catch (err) {
27 await connection.rollback();
28 throw err;
29 }
30}The stock decrement uses a conditional `WHERE stock >= quantity` to prevent overselling — if stock is insufficient for any item, the UPDATE affects 0 rows and the application rolls back the entire transaction. This prevents the classic race condition where two simultaneous orders both succeed against the same inventory.
Reproducible Dev Environment
One of the most practical decisions was investing in proper db:init and db:seed scripts. Any developer can clone the repo, run `npm run db:init` to create the schema and `npm run db:seed` to populate test data, and have a fully functional local instance in under a minute. Start-local.ps1 / Stop-local.ps1 PowerShell scripts handle the MySQL service and Node server together.
💡 Takeaway: Atomic database transactions aren't optional in e-commerce — they're the boundary between a trustworthy application and one that silently corrupts data under load. Design for transaction safety from the start, not as an afterthought.