liveProject

MOD — Engineering Deep Dive

A closer look at the engineering decisions behind MOD, including architecture, backend systems, authentication, API design and the technical trade-offs made throughout development.

MOD — Engineering Deep Dive
Diagram

System Architecture

Frontend

React

REST API

Express

Database

PostgreSQL

The project follows a traditional full-stack architecture, separating concerns between the client application, server and database.

Rather than relying on an external commerce platform, every major feature—from authentication to ordering—was designed as part of a custom backend.

The goal wasn't simply to build another online store. It was to understand how every layer of a modern commerce application works together.

Stack

Technology Stack

Frontend

React

React

UI Framework

Built the application around reusable components and predictable rendering.

Tailwind CSS

Styling

Created a consistent design system with utility-first styling.

The frontend was built with React and focuses on reusable UI, responsive layouts and modular state management.

Some of the key areas include:

Authentication flows Product browsing Wishlist management Shopping cart Checkout Responsive navigation Fuzzy search Custom filtering

Backend

React

Express

REST API

Implemented authentication, products, orders and wishlist endpoints.

PostgreSQL

Database

Stored users, products, orders and relationships using a relational model.

JWT

Authentication

Secured protected routes and user-specific resources.

The backend is built with Node.js and Express.

Responsibilities include:

  • User authentication
  • Product management
  • Wishlist persistence
  • Order processing
  • Search endpoints
  • Validation
  • Error handling
  • JWT authorization
Diagram

Authentication Flow

Login Form

REST API

JWT

Protected Route

Authentication is handled using JWT tokens.

The flow consists of:

Registration > Login > Token generation > Protected routes > User-specific resources || Features such as wishlists and ordering are only available after successful authentication.

Engineering

API Architecture

Rather than exposing database models directly, the application is organised around REST resources.

Examples include:

Authentication

Authentication, authorization and secure account management.

JWT

🔒 Protected

REST

POST/auth/register

Secure account creation, authentication and session management. The authentication layer protects customer-specific resources while keeping public catalogue endpoints openly accessible.

POST/auth/register

Create a new customer account.

POST/auth/login

Authenticate user credentials and issue a signed JWT.

GET/users/me

Return the authenticated user's profile.

Product Catalogue

The catalogue powers discovery across fashion and skincare products. It supports advanced filtering, category navigation, slug-based routing, featured collections and product recommendations.

Public

REST

Search

Filtering

GET/products/search/suggestions

Fuzzy search across product catalog.

GET/products/featured

Get feautured products

GET/products/bestsellers

Get Bestseller products

GET/products/category/{categorySlug}/subcategory/{subcategorySlug}

Get products by category and subcategory

GET/products/gender/{genderSlug}

Get products by gender

GET/products/slug/{slug}

Get a single product by slug

GET/products/{id}/related

Get related products

GET/products/{id}/availability

Check product availability

GET/products/{id}

Retrieve a single product.

PUT/products/{id}

Update a product (Admin Only)

DELETE/products/{id}

Delete a product (Admin Only)

GET/products

Get all products with filters

POST/products

Create a new product (Admin Only)

DELETE/products/{id}/permanent

Permanently delete a product - hard delete (Admin Only)

Orders

POST/api/orders

Create an order.

GET/api/orders

Retrieve user orders.

Engineering Slice

Fuzzy Product Search

Problem

Finding products should feel forgiving.

People rarely remember exact product names, spellings or even the correct category. Instead of requiring precise matches, the search experience was designed to interpret intent and surface relevant products even when the query is incomplete or slightly incorrect.

Approach

Rather than relying on SQL string matching, a dedicated fuzzy-search layer indexes multiple product attributes and assigns each a different weight.

This keeps the search logic reusable while producing results that feel much closer to how people naturally search online.

Implementation Journey

User types
Search Input
API Route: GET /products/search?q=...

Search Component

const [query, setQuery] = useState("");
 
useEffect(() => {
  const timer = setTimeout(() => {
    fetchProducts(query);
  }, 300);
 
  return () => clearTimeout(timer);
}, [query]);

The input is debounced before contacting the API. This dramatically reduces unnecessary requests while keeping the interface responsive.

Request
Express Route
Search Controller

products.routes.ts

router.get(
  "/search",
  productController.searchProducts
);

Keeping search behind its own endpoint allows the frontend to evolve independently from filtering, recommendations and category browsing.

Validate
Extract query
Search Service
JSON Response

products.controller.ts

export async function searchProducts(req, res) {
  const { q } = req.query;
 
  const results =
    await productService.search(q);
 
  res.json(results);
}

The controller only coordinates HTTP concerns. Business logic stays inside the service layer.

Products
Fuse Index
Weighted Matching
Sorted Results

search.service.ts

const fuse = new Fuse(products, {
  keys: [
    "name",
    "brand",
    "category",
    "tags",
  ],
  threshold: 0.35,
});
 
return fuse.search(query);

Each searchable field contributes differently to the final score. Product names have more influence than tags, producing more relevant matches while remaining tolerant of spelling mistakes.

Matched Products
Ranked
Returned
Rendered

Example Response

[
  {
    "name": "Premium Black Denim Jacket",
    "score": 0.02
  },
  {
    "name": "Black Bomber Jacket",
    "score": 0.08
  }
]

Only the most relevant products are returned to the client, keeping payloads small and rendering fast.

Outcome

✓ Forgiving search experience

✓ Better product discovery

✓ Modular search service

✓ Easy to extend with autocomplete, synonyms and AI ranking




CLIENT

Sign In

User
SignIn.jsx
useAuth()
AuthContext

Capturing user intent

Authentication begins at the interface.

The Sign In and Sign Up pages are intentionally lightweight. Their only responsibility is to collect user input, validate obvious mistakes on the client and hand the credentials to the authentication layer.

Keeping these components free from business logic means they remain easy to reason about and easy to reuse. They don't know how users are authenticated—they simply express the user's intent.

const handleSubmit = async (e) => {
  e.preventDefault();
 
  await login({
    email: formData.email,
    password: formData.password,
  });
 
  navigate(from, { replace: true });
}

APPLICATION

Authentication Context

SignIn.jsx
AuthContext
authService
Global User State

Separating interface from authentication

Instead of allowing pages to communicate directly with the API, every authentication request passes through the AuthContext.

This creates a single source of truth for the authenticated user. Once a login succeeds, every component—from the navigation bar to protected routes—reacts automatically without each page having to manage its own authentication state.

This separation also makes replacing the underlying authentication provider significantly easier in the future.

const login = async (credentials) => {
  const data = await authService.login(credentials)
 
  setUser(data.user)
  setIsAuthenticated(true)
 
  return data
}

CLIENT API

API Communication

AuthContext
authService
api.js
POST /auth/login

One gateway to the backend

Every request passes through a shared API layer.

Besides keeping HTTP requests consistent across the application, this abstraction automatically stores the returned JWT, restores existing sessions after refreshes and ensures future authenticated requests always include the required authorization token.

By centralising these responsibilities, the rest of the application never needs to know where authentication data is stored.

const data = await api.post(
  "/auth/login",
  credentials
)
 
localStorage.setItem(
  "token",
  data.token
)

SERVER

Authentication Pipeline

Routes
Validation
Controller
Service
Database

A predictable request pipeline

On the server every request follows the same architecture.

The router defines the available endpoint.

Validation middleware ensures incoming data satisfies the required schema.

The controller coordinates the request without containing business logic.

Finally, the service performs the authentication workflow by querying the database, verifying passwords and generating a JWT.

Keeping these responsibilities separate dramatically simplifies maintenance as the project grows.

router.post(
  "/login",
  loginValidator,
  validate,
  authController.login
)

DOMAIN

Identity Verification

Database
User
bcrypt.compare()
JWT
Authenticated User

Verifying identity securely

Passwords are never stored in plain text.

During registration each password is hashed using bcrypt before being persisted.

When logging in, the submitted password is compared against the stored hash rather than the original password.

Only after a successful comparison is a signed JSON Web Token created containing the user's identity and permissions.

This allows future requests to be authenticated without repeatedly querying the database.

const match = await bcrypt.compare(
  password,
  user.password_hash
)
 
const token = jwt.sign(
  payload,
  process.env.JWT_SECRET,
  {
    expiresIn: "7d",
  }
)

CLIENT

Protected Experience

User
Protected Route
JWT
Wishlist -> Cart -> Profile

Security as part of navigation

Rather than every page checking authentication independently, protected areas of the application are wrapped inside a dedicated ProtectedRoute component.

Unauthenticated users are redirected to the sign-in page while preserving their original destination. Once authentication succeeds they continue exactly where they intended to go.

This creates a smoother experience while ensuring sensitive functionality always remains behind authenticated routes.

if (
  requireAuth &&
  !isAuthenticated
) {
 
  return (
    <Navigate
      to="/signin"
      state={{
        from: location,
      }}
      replace
    />
  )
 
}



Challenges



Some of the biggest engineering challenges included:



Each challenge influenced how later features were implemented.





Looking Back



If rebuilding MOD today, there are several areas I would improve.

These include:



Despite these improvements, the project successfully achieved its original objective: designing and building a complete commerce platform from scratch while exploring the intersection of fashion, skincare and personalised shopping.

Next

Back to Part I

Return to the product story, design philosophy and user experience behind MOD.

Continue to Part II

Related Reading

3 connections