100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Node.js & Express Backend
95 minintermediate

Capstone — Production-Grade E-Commerce API

Capstone: Production-Grade E-Commerce API

This capstone project integrates every concept from the course into a single production-grade application: a fully-RESTful e-commerce API with dual versioning, JWT authentication with RBAC, file uploads to Cloudinary, transactional email via Nodemailer, BullMQ background jobs, OpenAPI documentation, Winston structured logging, Jest unit and integration tests, PM2 cluster configuration, and graceful shutdown. Building this project demonstrates that you can architect, implement, document, and deploy a real-world Node.js API from scratch.

Analogy🏏Cricket
🏏 Think of it like cricket: the capstone is your international match where every skill drilled all season must fire together in one innings. Just as a Test side blends batting, bowling, fielding, DRS reviews, and captaincy into a single coherent performance, this project fuses every course concept into one production-grade app — RESTful routes with dual versioning, JWT auth with RBAC, Cloudinary uploads, Nodemailer email, BullMQ background jobs, OpenAPI docs, Winston logging, Jest unit and integration tests, PM2 clustering, and graceful shutdown. And just as no player is picked for a single skill but for how their skills combine under real pressure, the capstone proves you can integrate these pieces into a system that holds up, not merely demonstrate each in isolation. The payoff: assembling every technique into one working application is the difference between having practised the drills and being ready to take the field — it's where separate lessons become a deployable engineer's craft.

You will build CricketStore API — a product catalogue and order management system for a cricket equipment shop. The API supports three user roles (customer, seller, admin), product image uploads with background processing, order confirmation emails, paginated product listings with filtering, and a complete OpenAPI documentation portal. The architecture is production-ready: structured logging, error handling, security middleware, and PM2 cluster configuration are all included from the start, not added as an afterthought.

Analogy🏏Cricket
🏏 Think of it like cricket: you're building CricketStore API, a product catalogue and order system for a cricket-equipment shop, and it's organised like a well-run club. Three user roles — customer, seller, admin — are your spectators, players, and match officials, each with different privileges, just as a fan can watch, a player can take the field, and an official can change the result. Product image uploads run with background processing, like sending kit off to be prepared without holding up the shop counter. Order confirmation emails are the receipts posted to members after every purchase, and paginated, filterable product listings are the neatly indexed equipment catalogue you can narrow by category or price the way you'd scan a squad list by role. The architecture underneath — structured logging, error handling — is the diligent scorer's box keeping everything traceable. The payoff: a role-aware, async-processing, well-catalogued API mirrors exactly how a real club separates spectators, players, and officials while keeping every transaction recorded.

Learning Objectives

  • Apply RESTful resource naming, HTTP verb semantics, pagination, HATEOAS links, and API versioning in a single coherent codebase
  • Implement JWT access/refresh token authentication with bcrypt password hashing and role-based access control
  • Handle multipart file uploads with Multer, process images with Sharp, and store them in Cloudinary using BullMQ background jobs
  • Send transactional emails (order confirmation, welcome email) asynchronously via a BullMQ email queue
  • Generate and serve interactive OpenAPI 3.0 documentation with swagger-jsdoc and swagger-ui-express
  • Configure Winston structured logging with Morgan HTTP request logging integrated into the same pipeline
  • Write unit tests with mocked dependencies and integration tests with mongodb-memory-server
  • Configure PM2 cluster mode with graceful shutdown, health check endpoint, and zero-downtime reload

Technical Requirements

The API must implement the following resource model: Users (register, login, refresh, logout, profile), Products (CRUD with image upload, pagination, filtering by category and price range, HATEOAS links), Orders (create from cart, list by user, update status by seller/admin), and Jobs (status endpoint for async operations). All state-changing operations require authentication. Products are publicly readable; creation requires seller role; deletion requires admin role. Orders are readable only by the owning customer or admin; status updates require seller or admin role.

Analogy🏏Cricket
🏏 Think of it like cricket: the resource model is your official team structure, each entity a clearly defined squad. Users handle register, login, refresh, logout, and profile — the players' registration and accreditation. Products offer full CRUD with image upload, pagination, filtering by category and price, and HATEOAS links — the equipment catalogue, browsable by anyone the way match fixtures are public. Orders are created from a cart and managed by seller or admin, like results recorded and adjusted only by officials. Jobs expose a status endpoint for async operations, the big screen showing work in progress. Crucially, every state-changing operation requires authentication — you must show your accreditation at the gate before you can alter anything, just as only credentialed players and officials may change the score, while spectators freely read the public product listings. The payoff: a clear resource model with auth gating every mutation means your API enforces exactly who may look versus who may change, the bedrock of a trustworthy production system.
bash
# Core dependencies
npm install express mongoose bcrypt jsonwebtoken cors helmet \
  express-rate-limit cookie-parser multer sharp cloudinary \
  streamifier bullmq ioredis nodemailer swagger-jsdoc \
  swagger-ui-express winston morgan uuid dotenv

# Dev dependencies
npm install --save-dev jest supertest mongodb-memory-server nodemon

Architecture

The project follows the feature-based directory structure established in Module 2, extended with dedicated directories for queues, workers, and tests. The app.js composition root mounts all routers, applies all middleware in the correct order, and exports the app without calling listen(). The server.js entry point binds the port and registers graceful shutdown handlers. Workers run as separate PM2-managed processes.

Analogy🏏Cricket
🏏 Think of it like cricket: the architecture follows a disciplined team structure. The feature-based directory layout from Module 2, extended with dedicated queues, workers, and tests directories, is like organising the club into specialised units — batting coaches, bowling coaches, physios — each in their own room. The app.js composition root is the team-sheet assembler: it mounts every router, applies all middleware in the correct order — like setting the batting order and field placements before play — and exports the app without calling listen(), the way a completed team-sheet defines the side but doesn't itself start the match. The server.js entry point is the umpire who actually binds the port and starts play, registering graceful shutdown handlers to close cleanly. Workers run as separate PM2 processes, the specialist net squad training apart from the live game. The payoff: separating composition (app.js) from startup (server.js) and isolating workers gives you a testable, restartable, cleanly-shutdown system — a side that's organised, not improvised.
bash
// Project structure
cricketstore-api/
  src/
    config/
      cloudinary.js    swagger.js    roles.js
    errors/
      AppError.js      NotFoundError.js
    middleware/
      authenticate.js  authorize.js  asyncHandler.js  requestId.js
    models/
      User.js          Product.js    Order.js
    queues/
      imageQueue.js    emailQueue.js
    routes/
      v1/
        auth.js        products.js   orders.js   jobs.js
        index.js
    services/
      authService.js   productService.js   orderService.js
    utils/
      jwt.js           logger.js     email.js    cloudinaryUpload.js
    app.js
  workers/
    imageWorker.js
    emailWorker.js
  tests/
    unit/
      services/        utils/
    integration/
      routes/
    setup.js
  ecosystem.config.js
  server.js

Phase 1: Foundation (Auth + Products Read)

Build auth routes (register, login, refresh, logout), the Product model with Mongoose, the GET endpoints for products with pagination and filtering, and the security middleware stack (Helmet, CORS, rate limiting). Verify with integration tests before proceeding to write operations.

Analogy🏏Cricket
🏏 Think of it like cricket: Phase 1 is laying the pitch and setting the boundary before any big innings. You build auth routes — register, login, refresh, logout — the accreditation gate every player must clear, then the Product model with Mongoose and the GET endpoints with pagination and filtering, your read-only public scorecard anyone can browse. Around it you erect the security middleware stack: Helmet, CORS, and rate limiting, the perimeter fencing, gate control, and crowd-flow limits that protect the ground before a single ball of write-traffic is bowled. And crucially you verify all of it with integration tests before proceeding to write operations, exactly as a captain insists the pitch and outfield pass inspection before committing the side to bat. The payoff: proving the foundation — auth, reads, and security — with real tests before building any write operations means you never stack risky mutations on unverified ground.
javascript
// models/Product.js
const mongoose = require('mongoose');
const productSchema = new mongoose.Schema({
  name:         { type: String, required: true, trim: true },
  description:  { type: String },
  price:        { type: Number, required: true, min: 0 },
  category:     { type: String, required: true, enum: ['bat','ball','helmet','gloves','pad','other'] },
  stock:        { type: Number, default: 0, min: 0 },
  seller:       { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  imageUrl:     { type: String, default: null },
  cloudinaryId: { type: String, default: null },
  imageStatus:  { type: String, enum: ['none','processing','ready'], default: 'none' }
}, { timestamps: true });
productSchema.index({ category: 1, price: 1 });
module.exports = mongoose.model('Product', productSchema);

// routes/v1/products.js — GET collection with pagination + filtering
router.get('/', asyncHandler(async (req, res) => {
  const { page = 1, limit = 12, category, minPrice, maxPrice, sort = '-createdAt' } = req.query;
  const filter = {};
  if (category)  filter.category = category;
  if (minPrice || maxPrice) filter.price = {};
  if (minPrice)  filter.price.$gte = parseFloat(minPrice);
  if (maxPrice)  filter.price.$lte = parseFloat(maxPrice);

  const [products, total] = await Promise.all([
    Product.find(filter).sort(sort).skip((page-1)*limit).limit(+limit).lean(),
    Product.countDocuments(filter)
  ]);

  const base = req.protocol + '://' + req.get('host') + '/api/v1/products';
  res.json({
    data:  products.map(p => ({ ...p, _links: { self: { href: base + '/' + p._id } } })),
    meta:  { total, page: +page, pages: Math.ceil(total/limit), limit: +limit },
    _links: {
      self:  { href: base + '?page=' + page },
      first: { href: base + '?page=1' },
      ...(+page > 1 && { prev: { href: base + '?page=' + (+page-1) } }),
      ...(+page < Math.ceil(total/limit) && { next: { href: base + '?page=' + (+page+1) } })
    }
  });
}));

Phase 2: Write Operations + File Upload Queue

Implement POST, PATCH, DELETE for products with RBAC enforcement. Add the image upload endpoint that accepts a file via Multer, adds a BullMQ job, and returns 202. Start the imageWorker process. Implement the order model and create/list routes. Add the email queue and emailWorker for order confirmation.

Analogy🏏Cricket
🏏 Think of it like cricket: Phase 2 is when your side goes from defending to actively scoring. You implement POST, PATCH, and DELETE for products with RBAC enforcement — the run-scoring strokes only accredited players may attempt, each shot checked against who's permitted to play it. The image upload endpoint takes a file via Multer, adds a BullMQ job, and returns 202 immediately — like a fielder gloving the ball and flicking it on without breaking stride — while the separately-started imageWorker does the slow processing off the field. Then you add the order model with create and list routes, and an email queue with an emailWorker firing order confirmations, the way the scorer records each completed transaction and posts the receipt afterwards without pausing play. The payoff: adding permission-checked writes plus queue-backed uploads and emails turns your read-only foundation into a fully active system that scores without ever stalling the batsman waiting on slow work.
javascript
// routes/v1/products.js — POST with image upload support
router.post('/',
  authenticate, authorize('write:products'),
  upload.single('image'),
  asyncHandler(async (req, res) => {
    const { name, description, price, category, stock } = req.body;
    if (!name || !price || !category)
      throw new AppError('name, price and category are required', 422);

    const product = await Product.create({
      name, description, price: parseFloat(price),
      category, stock: parseInt(stock || 0),
      seller: req.user.sub
    });

    if (req.file) {
      await imageQueue.add('process-image', {
        productId:   product.id.toString(),
        imageBuffer: req.file.buffer.toString('base64'),
        mimetype:    req.file.mimetype
      }, { attempts: 3, backoff: { type: 'exponential', delay: 3000 } });
    }

    res.status(201)
       .location('/api/v1/products/' + product.id)
       .json({ data: product });
  })
);

// models/Order.js
const orderSchema = new mongoose.Schema({
  customer:    { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true },
  items:       [{ product: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' }, qty: Number, price: Number }],
  total:       { type: Number, required: true },
  status:      { type: String, enum: ['pending','confirmed','shipped','delivered','cancelled'], default: 'pending' },
  address:     { type: String, required: true }
}, { timestamps: true });
module.exports = mongoose.model('Order', orderSchema);

Phase 3: Testing, Docs, and Production Config

Write unit tests for authService, productService, and pagination utility. Write integration tests for the auth routes and product routes using mongodb-memory-server. Generate OpenAPI documentation with swagger-jsdoc. Configure Winston + Morgan logging. Write ecosystem.config.js for PM2 cluster mode with graceful shutdown in server.js.

Analogy🏏Cricket
🏏 Think of it like cricket: Phase 3 is the pre-tour final preparation that makes the side genuinely match-ready. You write unit tests for authService, productService, and the pagination utility — focused net sessions on each individual skill — then integration tests for the auth and product routes using mongodb-memory-server, the full trial matches on a real practice pitch. You generate OpenAPI docs with swagger-jsdoc, publishing the official programme so every consumer knows the fixtures, and configure Winston plus Morgan logging, installing the diligent scorer who records every ball. Finally you write ecosystem.config.js for PM2 cluster mode with graceful shutdown in server.js — the captaincy plan for rotating bowlers across cores and closing play cleanly. The payoff: layering tests, published docs, structured logging, and clustered process management on top of a working app is exactly what turns a functioning prototype into a side you can confidently send out for a real production tour.
javascript
// tests/integration/products.test.js (sample)
const request = require('supertest');
const app     = require('../../src/app');
const Product = require('../../src/models/Product');
const User    = require('../../src/models/User');
const { signAccess } = require('../../src/utils/jwt');

let sellerToken, adminToken;

beforeAll(async () => {
  const seller = await User.create({ email: '[email protected]', passwordHash: 'x', role: 'seller' });
  const admin  = await User.create({ email: '[email protected]', passwordHash: 'x', role: 'admin' });
  sellerToken = signAccess({ sub: seller.id, role: 'seller' });
  adminToken  = signAccess({ sub: admin.id,  role: 'admin' });
});

describe('GET /api/v1/products', () => {
  beforeEach(async () => {
    await Product.insertMany([
      { name: 'Bat',    price: 120, category: 'bat',    seller: new mongoose.Types.ObjectId() },
      { name: 'Helmet', price: 85,  category: 'helmet', seller: new mongoose.Types.ObjectId() }
    ]);
  });

  it('returns paginated products with HATEOAS links', async () => {
    const res = await request(app).get('/api/v1/products?page=1&limit=10');
    expect(res.statusCode).toBe(200);
    expect(res.body.data.length).toBeGreaterThan(0);
    expect(res.body.data[0]._links.self.href).toContain('/api/v1/products/');
    expect(res.body.meta).toHaveProperty('total');
    expect(res.body._links).toHaveProperty('self');
  });
});

describe('POST /api/v1/products', () => {
  it('returns 401 without authentication', async () => {
    const res = await request(app).post('/api/v1/products').send({ name: 'X', price: 1, category: 'bat' });
    expect(res.statusCode).toBe(401);
  });

  it('creates product with seller token', async () => {
    const res = await request(app)
      .post('/api/v1/products')
      .set('Authorization', 'Bearer ' + sellerToken)
      .send({ name: 'Test Bat', price: 100, category: 'bat', stock: 10 });
    expect(res.statusCode).toBe(201);
    expect(res.headers.location).toContain('/api/v1/products/');
  });
});

Evaluation Rubric

  • Authentication (20 pts): register, login, refresh, logout all work correctly; tokens are short-lived; refresh uses rotation; logout clears cookie and DB hash
  • RBAC (15 pts): customer/seller/admin roles enforced; 401 for missing token; 403 for wrong role; permissions are defined in a central config, not scattered in route files
  • REST Design (15 pts): plural nouns, correct HTTP verbs, proper status codes (201 with Location, 204 for DELETE, 422 for validation); collection and item URLs consistent
  • Pagination and Filtering (10 pts): page, limit, category, minPrice, maxPrice all work; meta includes total and pages; HATEOAS collection links are accurate
  • File Upload + Queue (15 pts): POST /products/:id/image returns 202; imageWorker resizes and uploads to Cloudinary; imageStatus updates to 'ready' on completion
  • Testing (15 pts): unit tests for services with mocked DB; integration tests using mongodb-memory-server; at least 70% line coverage; unhappy paths tested
  • Production Config (10 pts): Winston JSON logging with requestId; Morgan piped to Winston; graceful shutdown in server.js; ecosystem.config.js with cluster mode

Submission checklist: All 7 rubric areas implemented. npm test passes with no errors. npm start starts the server on PORT env variable. The README.md includes setup steps, environment variable list, and curl examples for each major endpoint. The OpenAPI documentation is accessible at /api-docs when the server is running.

bash
# Final smoke test sequence
npm install
cp .env.example .env        # fill in your secrets
npm start &                 # start server in background

# Auth
TOKEN=$(curl -s -X POST :3000/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"[email protected]","password":"SecurePass123"}' | jq -r .accessToken)

# Create product
curl -X POST :3000/api/v1/products \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name":"Rohit Sharma Edition Bat","price":299,"category":"bat","stock":25}'

# List products with pagination
curl ":3000/api/v1/products?category=bat&page=1&limit=5&sort=-price"

# View documentation
open http://localhost:3000/api-docs

# Run tests
npm test

Submit your capstone project

Checking submission status…
Final Exam unlocks when all 36 lessons are complete (36 left)
Lesson 36 of 36
0% complete