Build a Microservices-based Application

Weekend Project: Creating an E-commerce Application with Microservices Architecture

Project Overview

In this weekend project, you will build a simplified e-commerce application using a microservices architecture. This hands-on project will help you apply the microservices principles we've learned this week and gain practical experience with service communication, API gateways, and message queues.

graph TD A[User Interface] --> B[API Gateway] B --> C[Product Service] B --> D[Cart Service] B --> E[Order Service] B --> F[User Service] E --> G[Notification Service] E -.-> H[Message Queue] H -.-> G C --> I[(Product Database)] D --> J[(Cart Database)] E --> K[(Order Database)] F --> L[(User Database)] style A fill:#e1f5fe,stroke:#0288d1 style B fill:#e8f5e9,stroke:#388e3c style C,D,E,F,G fill:#fff3e0,stroke:#f57c00 style H fill:#f3e5f5,stroke:#7b1fa2 style I,J,K,L fill:#e0f7fa,stroke:#00acc1

The application will consist of the following microservices:

Learning Objectives

Technology Stack

Project Structure

Let's follow George Polya's 4-step problem-solving method:

Step 1: Understand the Problem

We need to create a microservices-based e-commerce application where each service has a specific responsibility. The primary challenges include:

Step 2: Devise a Plan

  1. Create project structure with separate folders for each service
  2. Set up basic Express applications for each service
  3. Implement MongoDB connection for each service that requires a database
  4. Create RESTful APIs for each service
  5. Set up the API Gateway to route requests
  6. Implement inter-service communication
  7. Set up RabbitMQ for asynchronous communication
  8. Create a simple React frontend
  9. Configure Docker and Docker Compose
  10. Write tests for services

Project Folder Structure

microservices-ecommerce/
├── api-gateway/
├── product-service/
├── cart-service/
├── order-service/
├── user-service/
├── notification-service/
├── client/
├── docker-compose.yml
└── README.md
                

Step 3: Execute the Plan

Let's implement each component of our microservices application.

Setting Up the Service Template

First, let's create a base template for our microservices. Each service will follow a similar structure, with differences in their specific functionality.

Service Project Structure

service-name/
├── src/
│   ├── config/
│   │   └── database.js
│   ├── controllers/
│   ├── models/
│   ├── routes/
│   ├── services/
│   ├── utils/
│   └── app.js
├── tests/
├── Dockerfile
├── package.json
└── .env
                

Basic Service Setup

For each service, we'll create a basic Express application with MongoDB connection:

package.json (generic for each service)

{
  "name": "service-name",
  "version": "1.0.0",
  "description": "Service description",
  "main": "src/app.js",
  "scripts": {
    "start": "node src/app.js",
    "dev": "nodemon src/app.js",
    "test": "jest"
  },
  "dependencies": {
    "express": "^4.18.2",
    "mongoose": "^7.4.1",
    "dotenv": "^16.3.1",
    "cors": "^2.8.5",
    "axios": "^1.4.0",
    "amqplib": "^0.10.3",
    "winston": "^3.10.0"
  },
  "devDependencies": {
    "jest": "^29.6.2",
    "supertest": "^6.3.3",
    "nodemon": "^3.0.1"
  }
}
                

src/app.js (template for services)

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { connectToDatabase } = require('./config/database');

// Import routes
const serviceRoutes = require('./routes');

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(cors());
app.use(express.json());

// Routes
app.use('/api', serviceRoutes);

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Connect to database and start server
const startServer = async () => {
  try {
    await connectToDatabase();
    app.listen(PORT, () => {
      console.log(`Service running on port ${PORT}`);
    });
  } catch (error) {
    console.error('Failed to start service:', error);
    process.exit(1);
  }
};

startServer();

module.exports = app; // For testing
                

src/config/database.js

const mongoose = require('mongoose');

const connectToDatabase = async () => {
  try {
    const conn = await mongoose.connect(process.env.MONGODB_URI, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
    });
    console.log(`MongoDB Connected: ${conn.connection.host}`);
    return conn;
  } catch (error) {
    console.error(`Error connecting to MongoDB: ${error.message}`);
    throw error;
  }
};

module.exports = { connectToDatabase };
                

Dockerfile (template for each service)

FROM node:18-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]
                

.env (example)

PORT=3000
MONGODB_URI=mongodb://mongodb:27017/service-database
NODE_ENV=development
RABBITMQ_URL=amqp://rabbitmq:5672
                

Implementing the Product Service

The Product Service will manage the product catalog and inventory. It provides APIs to list, search, and get detailed information about products.

product-service/src/models/product.model.js

const mongoose = require('mongoose');

const productSchema = new mongoose.Schema({
  name: {
    type: String,
    required: true,
    trim: true
  },
  description: {
    type: String,
    required: true
  },
  price: {
    type: Number,
    required: true,
    min: 0
  },
  category: {
    type: String,
    required: true
  },
  inventory: {
    type: Number,
    required: true,
    min: 0,
    default: 0
  },
  imageUrl: {
    type: String,
    default: 'default-product.jpg'
  }
}, {
  timestamps: true
});

// Add index for searching products
productSchema.index({ name: 'text', description: 'text' });

const Product = mongoose.model('Product', productSchema);

module.exports = Product;
                

product-service/src/controllers/product.controller.js

const Product = require('../models/product.model');

// Get all products (with optional filtering)
exports.getProducts = async (req, res) => {
  try {
    const { category, search } = req.query;
    let query = {};
    
    if (category) {
      query.category = category;
    }
    
    if (search) {
      query.$text = { $search: search };
    }
    
    const products = await Product.find(query);
    res.status(200).json(products);
  } catch (error) {
    res.status(500).json({ message: 'Error fetching products', error: error.message });
  }
};

// Get a single product by ID
exports.getProductById = async (req, res) => {
  try {
    const product = await Product.findById(req.params.id);
    
    if (!product) {
      return res.status(404).json({ message: 'Product not found' });
    }
    
    res.status(200).json(product);
  } catch (error) {
    res.status(500).json({ message: 'Error fetching product', error: error.message });
  }
};

// Create a new product
exports.createProduct = async (req, res) => {
  try {
    const newProduct = new Product(req.body);
    const savedProduct = await newProduct.save();
    res.status(201).json(savedProduct);
  } catch (error) {
    res.status(400).json({ message: 'Error creating product', error: error.message });
  }
};

// Update a product
exports.updateProduct = async (req, res) => {
  try {
    const updatedProduct = await Product.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    );
    
    if (!updatedProduct) {
      return res.status(404).json({ message: 'Product not found' });
    }
    
    res.status(200).json(updatedProduct);
  } catch (error) {
    res.status(400).json({ message: 'Error updating product', error: error.message });
  }
};

// Delete a product
exports.deleteProduct = async (req, res) => {
  try {
    const product = await Product.findByIdAndDelete(req.params.id);
    
    if (!product) {
      return res.status(404).json({ message: 'Product not found' });
    }
    
    res.status(200).json({ message: 'Product deleted successfully' });
  } catch (error) {
    res.status(500).json({ message: 'Error deleting product', error: error.message });
  }
};

// Check if product is in stock
exports.checkStock = async (req, res) => {
  try {
    const { id, quantity } = req.query;
    
    if (!id || !quantity) {
      return res.status(400).json({ message: 'Product ID and quantity required' });
    }
    
    const product = await Product.findById(id);
    
    if (!product) {
      return res.status(404).json({ message: 'Product not found' });
    }
    
    const inStock = product.inventory >= parseInt(quantity);
    
    res.status(200).json({
      productId: id,
      inStock,
      availableQuantity: product.inventory
    });
  } catch (error) {
    res.status(500).json({ message: 'Error checking stock', error: error.message });
  }
};

// Update inventory (usually called by Order Service)
exports.updateInventory = async (req, res) => {
  try {
    const { productId, quantity } = req.body;
    
    if (!productId || !quantity) {
      return res.status(400).json({ message: 'Product ID and quantity required' });
    }
    
    const product = await Product.findById(productId);
    
    if (!product) {
      return res.status(404).json({ message: 'Product not found' });
    }
    
    if (product.inventory < quantity) {
      return res.status(400).json({ message: 'Not enough inventory' });
    }
    
    product.inventory -= quantity;
    await product.save();
    
    res.status(200).json({
      productId,
      newInventory: product.inventory,
      success: true
    });
  } catch (error) {
    res.status(500).json({ message: 'Error updating inventory', error: error.message });
  }
};
                

product-service/src/routes/product.routes.js

const express = require('express');
const productController = require('../controllers/product.controller');

const router = express.Router();

router.get('/', productController.getProducts);
router.get('/:id', productController.getProductById);
router.post('/', productController.createProduct);
router.put('/:id', productController.updateProduct);
router.delete('/:id', productController.deleteProduct);
router.get('/check-stock', productController.checkStock);
router.post('/update-inventory', productController.updateInventory);

module.exports = router;
                

product-service/src/routes/index.js

const express = require('express');
const productRoutes = require('./product.routes');

const router = express.Router();

router.use('/products', productRoutes);

module.exports = router;
                

Implementing the User Service

The User Service handles user authentication, registration, and profile management.

user-service/src/models/user.model.js

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true
  },
  password: {
    type: String,
    required: true
  },
  name: {
    type: String,
    required: true
  },
  address: {
    street: String,
    city: String,
    state: String,
    zipCode: String,
    country: String
  },
  role: {
    type: String,
    enum: ['user', 'admin'],
    default: 'user'
  }
}, {
  timestamps: true
});

// Hash password before saving
userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  try {
    const salt = await bcrypt.genSalt(10);
    this.password = await bcrypt.hash(this.password, salt);
    next();
  } catch (error) {
    next(error);
  }
});

// Method to check password
userSchema.methods.comparePassword = async function(password) {
  return await bcrypt.compare(password, this.password);
};

const User = mongoose.model('User', userSchema);

module.exports = User;
                

user-service/src/controllers/user.controller.js

const User = require('../models/user.model');
const jwt = require('jsonwebtoken');

// Register a new user
exports.register = async (req, res) => {
  try {
    const { username, email, password, name, address } = req.body;
    
    // Check if user already exists
    const existingUser = await User.findOne({ 
      $or: [{ email }, { username }] 
    });
    
    if (existingUser) {
      return res.status(400).json({ 
        message: 'User already exists with this email or username' 
      });
    }
    
    // Create new user
    const user = new User({
      username,
      email,
      password,
      name,
      address
    });
    
    await user.save();
    
    // Generate JWT token
    const token = jwt.sign(
      { id: user._id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '1d' }
    );
    
    res.status(201).json({
      message: 'User registered successfully',
      token,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        name: user.name,
        role: user.role
      }
    });
  } catch (error) {
    res.status(500).json({ 
      message: 'Error registering user', 
      error: error.message 
    });
  }
};

// Login user
exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;
    
    // Find user by email
    const user = await User.findOne({ email });
    
    if (!user) {
      return res.status(401).json({ message: 'Invalid credentials' });
    }
    
    // Check password
    const isMatch = await user.comparePassword(password);
    
    if (!isMatch) {
      return res.status(401).json({ message: 'Invalid credentials' });
    }
    
    // Generate JWT token
    const token = jwt.sign(
      { id: user._id, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '1d' }
    );
    
    res.status(200).json({
      message: 'Login successful',
      token,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        name: user.name,
        role: user.role
      }
    });
  } catch (error) {
    res.status(500).json({ 
      message: 'Error logging in', 
      error: error.message 
    });
  }
};

// Get user profile
exports.getProfile = async (req, res) => {
  try {
    const user = await User.findById(req.params.id).select('-password');
    
    if (!user) {
      return res.status(404).json({ message: 'User not found' });
    }
    
    res.status(200).json(user);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error fetching user profile', 
      error: error.message 
    });
  }
};

// Update user profile
exports.updateProfile = async (req, res) => {
  try {
    // Don't allow password updates through this endpoint
    if (req.body.password) {
      delete req.body.password;
    }
    
    const updatedUser = await User.findByIdAndUpdate(
      req.params.id,
      req.body,
      { new: true, runValidators: true }
    ).select('-password');
    
    if (!updatedUser) {
      return res.status(404).json({ message: 'User not found' });
    }
    
    res.status(200).json(updatedUser);
  } catch (error) {
    res.status(400).json({ 
      message: 'Error updating user profile', 
      error: error.message 
    });
  }
};

// Validate token (used by other services for authentication)
exports.validateToken = async (req, res) => {
  try {
    const { token } = req.body;
    
    if (!token) {
      return res.status(400).json({ message: 'Token is required' });
    }
    
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    const user = await User.findById(decoded.id).select('-password');
    
    if (!user) {
      return res.status(401).json({ message: 'Invalid token - user not found' });
    }
    
    res.status(200).json({
      valid: true,
      user: {
        id: user._id,
        username: user.username,
        email: user.email,
        name: user.name,
        role: user.role
      }
    });
  } catch (error) {
    res.status(401).json({ 
      valid: false, 
      message: 'Invalid token', 
      error: error.message 
    });
  }
};
                

user-service/src/routes/user.routes.js

const express = require('express');
const userController = require('../controllers/user.controller');

const router = express.Router();

router.post('/register', userController.register);
router.post('/login', userController.login);
router.get('/:id', userController.getProfile);
router.put('/:id', userController.updateProfile);
router.post('/validate-token', userController.validateToken);

module.exports = router;
                

user-service/src/routes/index.js

const express = require('express');
const userRoutes = require('./user.routes');

const router = express.Router();

router.use('/users', userRoutes);

module.exports = router;
                

user-service/.env

PORT=3001
MONGODB_URI=mongodb://mongodb:27017/user-service
JWT_SECRET=your_jwt_secret_key
NODE_ENV=development
                

Implementing the Cart Service

The Cart Service manages shopping carts for users, including adding, updating, and removing items from the cart.

cart-service/src/models/cart.model.js

const mongoose = require('mongoose');

const cartItemSchema = new mongoose.Schema({
  productId: {
    type: String,
    required: true
  },
  quantity: {
    type: Number,
    required: true,
    min: 1
  },
  name: {
    type: String,
    required: true
  },
  price: {
    type: Number,
    required: true
  },
  imageUrl: {
    type: String
  }
});

const cartSchema = new mongoose.Schema({
  userId: {
    type: String,
    required: true,
    unique: true
  },
  items: [cartItemSchema],
  createdAt: {
    type: Date,
    default: Date.now,
    expires: '30d' // Cart expires after 30 days of inactivity
  },
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

// Update timestamp on save
cartSchema.pre('save', function(next) {
  this.updatedAt = Date.now();
  next();
});

const Cart = mongoose.model('Cart', cartSchema);

module.exports = Cart;
                

cart-service/src/services/product.service.js

const axios = require('axios');

const PRODUCT_SERVICE_URL = process.env.PRODUCT_SERVICE_URL || 'http://product-service:3000/api/products';

// Get product details from the Product Service
exports.getProductDetails = async (productId) => {
  try {
    const response = await axios.get(`${PRODUCT_SERVICE_URL}/${productId}`);
    return response.data;
  } catch (error) {
    console.error(`Error fetching product details: ${error.message}`);
    throw new Error('Failed to get product details');
  }
};

// Check if product is in stock
exports.checkProductStock = async (productId, quantity) => {
  try {
    const response = await axios.get(`${PRODUCT_SERVICE_URL}/check-stock?id=${productId}&quantity=${quantity}`);
    return response.data;
  } catch (error) {
    console.error(`Error checking product stock: ${error.message}`);
    throw new Error('Failed to check product stock');
  }
};
                

cart-service/src/controllers/cart.controller.js

const Cart = require('../models/cart.model');
const productService = require('../services/product.service');

// Get user's cart
exports.getCart = async (req, res) => {
  try {
    const { userId } = req.params;
    
    let cart = await Cart.findOne({ userId });
    
    if (!cart) {
      // Create empty cart if none exists
      cart = new Cart({
        userId,
        items: []
      });
      await cart.save();
    }
    
    res.status(200).json(cart);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error fetching cart', 
      error: error.message 
    });
  }
};

// Add item to cart
exports.addItem = async (req, res) => {
  try {
    const { userId } = req.params;
    const { productId, quantity } = req.body;
    
    if (!productId || !quantity) {
      return res.status(400).json({ 
        message: 'Product ID and quantity are required' 
      });
    }
    
    // Check if product exists and is in stock
    const stockCheck = await productService.checkProductStock(productId, quantity);
    
    if (!stockCheck.inStock) {
      return res.status(400).json({ 
        message: `Product is out of stock. Only ${stockCheck.availableQuantity} available.` 
      });
    }
    
    // Get product details
    const product = await productService.getProductDetails(productId);
    
    // Find or create cart
    let cart = await Cart.findOne({ userId });
    
    if (!cart) {
      cart = new Cart({
        userId,
        items: []
      });
    }
    
    // Check if item already exists in cart
    const existingItemIndex = cart.items.findIndex(
      item => item.productId === productId
    );
    
    if (existingItemIndex > -1) {
      // Update quantity if item exists
      cart.items[existingItemIndex].quantity += quantity;
    } else {
      // Add new item to cart
      cart.items.push({
        productId,
        quantity,
        name: product.name,
        price: product.price,
        imageUrl: product.imageUrl
      });
    }
    
    await cart.save();
    
    res.status(200).json(cart);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error adding item to cart', 
      error: error.message 
    });
  }
};

// Update item quantity
exports.updateItemQuantity = async (req, res) => {
  try {
    const { userId, productId } = req.params;
    const { quantity } = req.body;
    
    if (!quantity || quantity < 1) {
      return res.status(400).json({ 
        message: 'Valid quantity is required' 
      });
    }
    
    // Check if product is in stock
    const stockCheck = await productService.checkProductStock(productId, quantity);
    
    if (!stockCheck.inStock) {
      return res.status(400).json({ 
        message: `Not enough stock. Only ${stockCheck.availableQuantity} available.` 
      });
    }
    
    // Find cart
    const cart = await Cart.findOne({ userId });
    
    if (!cart) {
      return res.status(404).json({ message: 'Cart not found' });
    }
    
    // Find item in cart
    const itemIndex = cart.items.findIndex(
      item => item.productId === productId
    );
    
    if (itemIndex === -1) {
      return res.status(404).json({ message: 'Item not found in cart' });
    }
    
    // Update quantity
    cart.items[itemIndex].quantity = quantity;
    await cart.save();
    
    res.status(200).json(cart);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error updating item quantity', 
      error: error.message 
    });
  }
};

// Remove item from cart
exports.removeItem = async (req, res) => {
  try {
    const { userId, productId } = req.params;
    
    // Find cart
    const cart = await Cart.findOne({ userId });
    
    if (!cart) {
      return res.status(404).json({ message: 'Cart not found' });
    }
    
    // Remove item from cart
    cart.items = cart.items.filter(
      item => item.productId !== productId
    );
    
    await cart.save();
    
    res.status(200).json(cart);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error removing item from cart', 
      error: error.message 
    });
  }
};

// Clear cart
exports.clearCart = async (req, res) => {
  try {
    const { userId } = req.params;
    
    // Find cart
    const cart = await Cart.findOne({ userId });
    
    if (!cart) {
      return res.status(404).json({ message: 'Cart not found' });
    }
    
    // Clear items
    cart.items = [];
    await cart.save();
    
    res.status(200).json({ 
      message: 'Cart cleared successfully', 
      cart 
    });
  } catch (error) {
    res.status(500).json({ 
      message: 'Error clearing cart', 
      error: error.message 
    });
  }
};
                

cart-service/src/routes/cart.routes.js

const express = require('express');
const cartController = require('../controllers/cart.controller');

const router = express.Router();

router.get('/:userId', cartController.getCart);
router.post('/:userId/items', cartController.addItem);
router.put('/:userId/items/:productId', cartController.updateItemQuantity);
router.delete('/:userId/items/:productId', cartController.removeItem);
router.delete('/:userId', cartController.clearCart);

module.exports = router;
                

cart-service/src/routes/index.js

const express = require('express');
const cartRoutes = require('./cart.routes');

const router = express.Router();

router.use('/carts', cartRoutes);

module.exports = router;
                

cart-service/.env

PORT=3002
MONGODB_URI=mongodb://mongodb:27017/cart-service
NODE_ENV=development
PRODUCT_SERVICE_URL=http://product-service:3000/api/products
                

Implementing the Order Service

The Order Service handles order creation, payment processing, and order status updates.

order-service/src/models/order.model.js

const mongoose = require('mongoose');

const orderItemSchema = new mongoose.Schema({
  productId: {
    type: String,
    required: true
  },
  quantity: {
    type: Number,
    required: true,
    min: 1
  },
  price: {
    type: Number,
    required: true
  },
  name: {
    type: String,
    required: true
  }
});

const orderSchema = new mongoose.Schema({
  userId: {
    type: String,
    required: true
  },
  items: [orderItemSchema],
  totalAmount: {
    type: Number,
    required: true
  },
  shippingAddress: {
    street: String,
    city: String,
    state: String,
    zipCode: String,
    country: String
  },
  status: {
    type: String,
    enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'],
    default: 'pending'
  },
  paymentStatus: {
    type: String,
    enum: ['pending', 'paid', 'failed'],
    default: 'pending'
  },
  paymentMethod: {
    type: String,
    required: true
  }
}, {
  timestamps: true
});

const Order = mongoose.model('Order', orderSchema);

module.exports = Order;
                

order-service/src/services/cart.service.js

const axios = require('axios');

const CART_SERVICE_URL = process.env.CART_SERVICE_URL || 'http://cart-service:3002/api/carts';

// Get user's cart
exports.getUserCart = async (userId) => {
  try {
    const response = await axios.get(`${CART_SERVICE_URL}/${userId}`);
    return response.data;
  } catch (error) {
    console.error(`Error fetching user cart: ${error.message}`);
    throw new Error('Failed to get user cart');
  }
};

// Clear user's cart after order is placed
exports.clearCart = async (userId) => {
  try {
    const response = await axios.delete(`${CART_SERVICE_URL}/${userId}`);
    return response.data;
  } catch (error) {
    console.error(`Error clearing user cart: ${error.message}`);
    throw new Error('Failed to clear user cart');
  }
};
                

order-service/src/services/product.service.js

const axios = require('axios');

const PRODUCT_SERVICE_URL = process.env.PRODUCT_SERVICE_URL || 'http://product-service:3000/api/products';

// Update product inventory after order
exports.updateInventory = async (productId, quantity) => {
  try {
    const response = await axios.post(`${PRODUCT_SERVICE_URL}/update-inventory`, {
      productId,
      quantity
    });
    return response.data;
  } catch (error) {
    console.error(`Error updating product inventory: ${error.message}`);
    throw new Error('Failed to update product inventory');
  }
};

// Check if product is in stock
exports.checkStock = async (productId, quantity) => {
  try {
    const response = await axios.get(
      `${PRODUCT_SERVICE_URL}/check-stock?id=${productId}&quantity=${quantity}`
    );
    return response.data;
  } catch (error) {
    console.error(`Error checking product stock: ${error.message}`);
    throw new Error('Failed to check product stock');
  }
};
                

order-service/src/services/user.service.js

const axios = require('axios');

const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://user-service:3001/api/users';

// Get user details
exports.getUserDetails = async (userId) => {
  try {
    const response = await axios.get(`${USER_SERVICE_URL}/${userId}`);
    return response.data;
  } catch (error) {
    console.error(`Error fetching user details: ${error.message}`);
    throw new Error('Failed to get user details');
  }
};
                

order-service/src/services/message-queue.js

const amqp = require('amqplib');

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://rabbitmq:5672';
const ORDER_QUEUE = 'order_events';

let channel;

// Connect to RabbitMQ
const connectToMessageQueue = async () => {
  try {
    const connection = await amqp.connect(RABBITMQ_URL);
    channel = await connection.createChannel();
    
    // Assert queue exists
    await channel.assertQueue(ORDER_QUEUE, {
      durable: true
    });
    
    console.log('Connected to RabbitMQ');
    return channel;
  } catch (error) {
    console.error(`Error connecting to RabbitMQ: ${error.message}`);
    throw error;
  }
};

// Send message to queue
const sendToQueue = async (message) => {
  try {
    if (!channel) {
      await connectToMessageQueue();
    }
    
    channel.sendToQueue(ORDER_QUEUE, Buffer.from(JSON.stringify(message)), {
      persistent: true
    });
    
    console.log('Message sent to queue:', message);
  } catch (error) {
    console.error(`Error sending message to queue: ${error.message}`);
    throw error;
  }
};

module.exports = {
  connectToMessageQueue,
  sendToQueue
};
                

order-service/src/controllers/order.controller.js

const Order = require('../models/order.model');
const cartService = require('../services/cart.service');
const productService = require('../services/product.service');
const userService = require('../services/user.service');
const messageQueue = require('../services/message-queue');

// Create a new order
exports.createOrder = async (req, res) => {
  try {
    const { userId, paymentMethod } = req.body;
    
    if (!userId || !paymentMethod) {
      return res.status(400).json({ 
        message: 'User ID and payment method are required' 
      });
    }
    
    // Get user cart
    const cart = await cartService.getUserCart(userId);
    
    if (!cart || !cart.items || cart.items.length === 0) {
      return res.status(400).json({ message: 'Cart is empty' });
    }
    
    // Get user details for shipping address
    const user = await userService.getUserDetails(userId);
    
    // Check if all items are in stock
    for (const item of cart.items) {
      const stockCheck = await productService.checkStock(
        item.productId, 
        item.quantity
      );
      
      if (!stockCheck.inStock) {
        return res.status(400).json({ 
          message: `${item.name} is out of stock. Only ${stockCheck.availableQuantity} available.` 
        });
      }
    }
    
    // Calculate total amount
    const totalAmount = cart.items.reduce(
      (total, item) => total + (item.price * item.quantity), 
      0
    );
    
    // Create new order
    const order = new Order({
      userId,
      items: cart.items,
      totalAmount,
      shippingAddress: user.address,
      paymentMethod
    });
    
    await order.save();
    
    // Update inventory
    for (const item of cart.items) {
      await productService.updateInventory(item.productId, item.quantity);
    }
    
    // Clear cart
    await cartService.clearCart(userId);
    
    // Send order created event to message queue
    await messageQueue.sendToQueue({
      type: 'ORDER_CREATED',
      orderId: order._id,
      userId,
      orderDetails: {
        items: order.items,
        totalAmount: order.totalAmount,
        status: order.status
      }
    });
    
    res.status(201).json(order);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error creating order', 
      error: error.message 
    });
  }
};

// Get order by ID
exports.getOrderById = async (req, res) => {
  try {
    const order = await Order.findById(req.params.id);
    
    if (!order) {
      return res.status(404).json({ message: 'Order not found' });
    }
    
    res.status(200).json(order);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error fetching order', 
      error: error.message 
    });
  }
};

// Get user orders
exports.getUserOrders = async (req, res) => {
  try {
    const { userId } = req.params;
    const orders = await Order.find({ userId }).sort({ createdAt: -1 });
    
    res.status(200).json(orders);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error fetching user orders', 
      error: error.message 
    });
  }
};

// Update order status
exports.updateOrderStatus = async (req, res) => {
  try {
    const { status } = req.body;
    
    if (!status) {
      return res.status(400).json({ message: 'Status is required' });
    }
    
    const order = await Order.findById(req.params.id);
    
    if (!order) {
      return res.status(404).json({ message: 'Order not found' });
    }
    
    order.status = status;
    await order.save();
    
    // Send order updated event to message queue
    await messageQueue.sendToQueue({
      type: 'ORDER_UPDATED',
      orderId: order._id,
      userId: order.userId,
      status: order.status
    });
    
    res.status(200).json(order);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error updating order status', 
      error: error.message 
    });
  }
};

// Process payment for an order
exports.processPayment = async (req, res) => {
  try {
    const { paymentDetails } = req.body;
    
    if (!paymentDetails) {
      return res.status(400).json({ message: 'Payment details are required' });
    }
    
    const order = await Order.findById(req.params.id);
    
    if (!order) {
      return res.status(404).json({ message: 'Order not found' });
    }
    
    // In a real application, this would integrate with a payment gateway
    // For this example, we'll simulate successful payment
    
    // Update payment status
    order.paymentStatus = 'paid';
    order.status = 'processing';
    await order.save();
    
    // Send payment processed event to message queue
    await messageQueue.sendToQueue({
      type: 'PAYMENT_PROCESSED',
      orderId: order._id,
      userId: order.userId,
      amount: order.totalAmount
    });
    
    res.status(200).json({
      message: 'Payment processed successfully',
      order
    });
  } catch (error) {
    res.status(500).json({ 
      message: 'Error processing payment', 
      error: error.message 
    });
  }
};
                

order-service/src/routes/order.routes.js

const express = require('express');
const orderController = require('../controllers/order.controller');

const router = express.Router();

router.post('/', orderController.createOrder);
router.get('/:id', orderController.getOrderById);
router.get('/user/:userId', orderController.getUserOrders);
router.put('/:id/status', orderController.updateOrderStatus);
router.post('/:id/payment', orderController.processPayment);

module.exports = router;
                

order-service/src/routes/index.js

const express = require('express');
const orderRoutes = require('./order.routes');

const router = express.Router();

router.use('/orders', orderRoutes);

module.exports = router;
                

order-service/.env

PORT=3003
MONGODB_URI=mongodb://mongodb:27017/order-service
NODE_ENV=development
CART_SERVICE_URL=http://cart-service:3002/api/carts
PRODUCT_SERVICE_URL=http://product-service:3000/api/products
USER_SERVICE_URL=http://user-service:3001/api/users
RABBITMQ_URL=amqp://rabbitmq:5672
                

Implementing the Notification Service

The Notification Service listens to events from the message queue and sends notifications to users based on order events.

notification-service/src/models/notification.model.js

const mongoose = require('mongoose');

const notificationSchema = new mongoose.Schema({
  userId: {
    type: String,
    required: true
  },
  type: {
    type: String,
    enum: ['order_created', 'order_updated', 'payment_processed'],
    required: true
  },
  title: {
    type: String,
    required: true
  },
  message: {
    type: String,
    required: true
  },
  relatedId: {
    type: String,
    required: true
  },
  read: {
    type: Boolean,
    default: false
  }
}, {
  timestamps: true
});

const Notification = mongoose.model('Notification', notificationSchema);

module.exports = Notification;
                

notification-service/src/services/message-queue.js

const amqp = require('amqplib');
const notificationController = require('../controllers/notification.controller');

const RABBITMQ_URL = process.env.RABBITMQ_URL || 'amqp://rabbitmq:5672';
const ORDER_QUEUE = 'order_events';

// Connect to RabbitMQ and consume messages
const connectToMessageQueue = async () => {
  try {
    const connection = await amqp.connect(RABBITMQ_URL);
    const channel = await connection.createChannel();
    
    // Assert queue exists
    await channel.assertQueue(ORDER_QUEUE, {
      durable: true
    });
    
    console.log('Connected to RabbitMQ, waiting for messages...');
    
    // Consume messages
    channel.consume(ORDER_QUEUE, async (message) => {
      if (message !== null) {
        try {
          const content = JSON.parse(message.content.toString());
          console.log('Received message:', content);
          
          // Process message based on type
          switch (content.type) {
            case 'ORDER_CREATED':
              await notificationController.createOrderCreatedNotification(content);
              break;
            case 'ORDER_UPDATED':
              await notificationController.createOrderUpdatedNotification(content);
              break;
            case 'PAYMENT_PROCESSED':
              await notificationController.createPaymentProcessedNotification(content);
              break;
            default:
              console.log('Unknown message type:', content.type);
          }
          
          // Acknowledge message
          channel.ack(message);
        } catch (error) {
          console.error('Error processing message:', error);
          // Reject message and requeue
          channel.nack(message, false, true);
        }
      }
    });
    
    return channel;
  } catch (error) {
    console.error(`Error connecting to RabbitMQ: ${error.message}`);
    throw error;
  }
};

module.exports = {
  connectToMessageQueue
};
                

notification-service/src/services/email.service.js

// In a real application, this would integrate with an email service provider
// For this example, we'll simulate sending emails

exports.sendEmail = async (to, subject, body) => {
  // Simulate email sending
  console.log(`Sending email to ${to}`);
  console.log(`Subject: ${subject}`);
  console.log(`Body: ${body}`);
  
  // In a real application, you would use a library like nodemailer
  // or a service like SendGrid or AWS SES
  
  return {
    success: true,
    messageId: `mock_${Math.random().toString(36).substring(2, 15)}`
  };
};
                

notification-service/src/services/user.service.js

const axios = require('axios');

const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://user-service:3001/api/users';

// Get user details
exports.getUserDetails = async (userId) => {
  try {
    const response = await axios.get(`${USER_SERVICE_URL}/${userId}`);
    return response.data;
  } catch (error) {
    console.error(`Error fetching user details: ${error.message}`);
    throw new Error('Failed to get user details');
  }
};
                

notification-service/src/controllers/notification.controller.js

const Notification = require('../models/notification.model');
const emailService = require('../services/email.service');
const userService = require('../services/user.service');

// Create notification for order created event
exports.createOrderCreatedNotification = async (eventData) => {
  const { orderId, userId, orderDetails } = eventData;
  
  try {
    // Get user details for email
    const user = await userService.getUserDetails(userId);
    
    // Create notification
    const notification = new Notification({
      userId,
      type: 'order_created',
      title: 'Order Placed',
      message: `Your order #${orderId} has been placed successfully.`,
      relatedId: orderId
    });
    
    await notification.save();
    
    // Send email notification
    await emailService.sendEmail(
      user.email,
      'Order Confirmation',
      `Hello ${user.name},\n\nYour order #${orderId} has been placed successfully.\n\nTotal: $${orderDetails.totalAmount}\n\nThank you for shopping with us!`
    );
    
    return notification;
  } catch (error) {
    console.error(`Error creating order created notification: ${error.message}`);
    throw error;
  }
};

// Create notification for order updated event
exports.createOrderUpdatedNotification = async (eventData) => {
  const { orderId, userId, status } = eventData;
  
  try {
    // Get user details for email
    const user = await userService.getUserDetails(userId);
    
    // Format status for display
    const formattedStatus = status.charAt(0).toUpperCase() + status.slice(1);
    
    // Create notification
    const notification = new Notification({
      userId,
      type: 'order_updated',
      title: 'Order Status Updated',
      message: `Your order #${orderId} has been updated to: ${formattedStatus}`,
      relatedId: orderId
    });
    
    await notification.save();
    
    // Send email notification
    await emailService.sendEmail(
      user.email,
      'Order Status Update',
      `Hello ${user.name},\n\nYour order #${orderId} has been updated to: ${formattedStatus}\n\nThank you for shopping with us!`
    );
    
    return notification;
  } catch (error) {
    console.error(`Error creating order updated notification: ${error.message}`);
    throw error;
  }
};

// Create notification for payment processed event
exports.createPaymentProcessedNotification = async (eventData) => {
  const { orderId, userId, amount } = eventData;
  
  try {
    // Get user details for email
    const user = await userService.getUserDetails(userId);
    
    // Create notification
    const notification = new Notification({
      userId,
      type: 'payment_processed',
      title: 'Payment Processed',
      message: `Your payment of $${amount} for order #${orderId} has been processed successfully.`,
      relatedId: orderId
    });
    
    await notification.save();
    
    // Send email notification
    await emailService.sendEmail(
      user.email,
      'Payment Confirmation',
      `Hello ${user.name},\n\nYour payment of $${amount} for order #${orderId} has been processed successfully.\n\nThank you for shopping with us!`
    );
    
    return notification;
  } catch (error) {
    console.error(`Error creating payment processed notification: ${error.message}`);
    throw error;
  }
};

// Get user notifications
exports.getUserNotifications = async (req, res) => {
  try {
    const { userId } = req.params;
    const notifications = await Notification.find({ userId })
      .sort({ createdAt: -1 });
    
    res.status(200).json(notifications);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error fetching notifications', 
      error: error.message 
    });
  }
};

// Mark notification as read
exports.markAsRead = async (req, res) => {
  try {
    const notification = await Notification.findByIdAndUpdate(
      req.params.id,
      { read: true },
      { new: true }
    );
    
    if (!notification) {
      return res.status(404).json({ message: 'Notification not found' });
    }
    
    res.status(200).json(notification);
  } catch (error) {
    res.status(500).json({ 
      message: 'Error marking notification as read', 
      error: error.message 
    });
  }
};
                

notification-service/src/routes/notification.routes.js

const express = require('express');
const notificationController = require('../controllers/notification.controller');

const router = express.Router();

router.get('/user/:userId', notificationController.getUserNotifications);
router.put('/:id/read', notificationController.markAsRead);

module.exports = router;
                

notification-service/src/routes/index.js

const express = require('express');
const notificationRoutes = require('./notification.routes');

const router = express.Router();

router.use('/notifications', notificationRoutes);

module.exports = router;
                

notification-service/src/app.js

require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { connectToDatabase } = require('./config/database');
const messageQueue = require('./services/message-queue');

// Import routes
const notificationRoutes = require('./routes');

const app = express();
const PORT = process.env.PORT || 3004;

// Middleware
app.use(cors());
app.use(express.json());

// Routes
app.use('/api', notificationRoutes);

// Health check endpoint
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok' });
});

// Connect to database, message queue, and start server
const startServer = async () => {
  try {
    await connectToDatabase();
    await messageQueue.connectToMessageQueue();
    
    app.listen(PORT, () => {
      console.log(`Notification Service running on port ${PORT}`);
    });
  } catch (error) {
    console.error('Failed to start service:', error);
    process.exit(1);
  }
};

startServer();

module.exports = app; // For testing
                

notification-service/.env

PORT=3004
MONGODB_URI=mongodb://mongodb:27017/notification-service
NODE_ENV=development
USER_SERVICE_URL=http://user-service:3001/api/users
RABBITMQ_URL=amqp://rabbitmq:5672
                

Implementing the API Gateway

The API Gateway provides a single entry point for all client requests, routing them to the appropriate microservices.

api-gateway/src/services/auth.service.js

const axios = require('axios');

const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://user-service:3001/api/users';

// Validate JWT token
exports.validateToken = async (token) => {
  try {
    const response = await axios.post(`${USER_SERVICE_URL}/validate-token`, { token });
    return response.data;
  } catch (error) {
    console.error(`Error validating token: ${error.message}`);
    return { valid: false, message: 'Invalid token' };
  }
};
                

api-gateway/src/middlewares/auth.middleware.js


  const authService = require('../services/auth.service');
  
  // Authentication middleware
  exports.authenticate = async (req, res, next) => {
    try {
      // Get token from headers
      const authHeader = req.headers.authorization;
      
      if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return res.status(401).json({ message: 'Authentication required' });
      }
      
      const token = authHeader.split(' ')[1];
      const validationResult = await authService.validateToken(token);
      
      if (!validationResult.valid) {
        return res.status(401).json({ message: 'Invalid or expired token' });
      }
      
      // Add user info to request for downstream use
      req.user = validationResult.user;
      next();
    } catch (error) {
      console.error(`Auth middleware error: ${error.message}`);
      res.status(500).json({ message: 'Authentication failed' });
    }
  };
  
  // Admin authorization middleware
  exports.authorizeAdmin = async (req, res, next) => {
    if (!req.user || req.user.role !== 'admin') {
      return res.status(403).json({ message: 'Access denied. Admin privileges required.' });
    }
    
    next();
  };
                  

api-gateway/src/routes/proxy.routes.js

  const express = require('express');
  const { createProxyMiddleware } = require('http-proxy-middleware');
  const { authenticate, authorizeAdmin } = require('../middlewares/auth.middleware');
  
  const router = express.Router();
  
  // Service URLs
  const PRODUCT_SERVICE_URL = process.env.PRODUCT_SERVICE_URL || 'http://product-service:3000';
  const USER_SERVICE_URL = process.env.USER_SERVICE_URL || 'http://user-service:3001';
  const CART_SERVICE_URL = process.env.CART_SERVICE_URL || 'http://cart-service:3002';
  const ORDER_SERVICE_URL = process.env.ORDER_SERVICE_URL || 'http://order-service:3003';
  const NOTIFICATION_SERVICE_URL = process.env.NOTIFICATION_SERVICE_URL || 'http://notification-service:3004';
  
  // Proxy options
  const defaultOptions = {
    changeOrigin: true,
    pathRewrite: {
      '^/api/v1': '/api'
    },
    onProxyReq: (proxyReq, req, res) => {
      // Add user ID from auth token if available
      if (req.user) {
        proxyReq.setHeader('X-User-Id', req.user.id);
        proxyReq.setHeader('X-User-Role', req.user.role);
      }
    },
    logLevel: process.env.NODE_ENV === 'development' ? 'debug' : 'info'
  };
  
  // Public routes (no authentication required)
  // Auth routes
  router.use(
    '/api/v1/auth',
    createProxyMiddleware({
      ...defaultOptions,
      target: USER_SERVICE_URL,
      pathRewrite: {
        '^/api/v1/auth': '/api/users'
      }
    })
  );
  
  // Product routes (read only)
  router.use(
    '/api/v1/products',
    createProxyMiddleware({
      ...defaultOptions,
      target: PRODUCT_SERVICE_URL,
      pathRewrite: {
        '^/api/v1/products': '/api/products'
      }
    })
  );
  
  // Protected routes (authentication required)
  // Cart routes
  router.use(
    '/api/v1/cart',
    authenticate,
    (req, res, next) => {
      // Use a custom route handler that dynamically creates the proxy with user ID
      const userId = req.user.id;
      const cartProxy = createProxyMiddleware({
        ...defaultOptions,
        target: CART_SERVICE_URL,
        pathRewrite: {
          [`^/api/v1/cart`]: `/api/carts/${userId}`
        }
      });
      
      cartProxy(req, res, next);
    }
  );
  
  // Order routes
  router.use(
    '/api/v1/orders',
    authenticate,
    createProxyMiddleware({
      ...defaultOptions,
      target: ORDER_SERVICE_URL,
      pathRewrite: {
        '^/api/v1/orders': '/api/orders'
      }
    })
  );
  
  // User profile routes
  router.use(
    '/api/v1/profile',
    authenticate,
    (req, res, next) => {
      // Use a custom route handler that dynamically creates the proxy with user ID
      const userId = req.user.id;
      const profileProxy = createProxyMiddleware({
        ...defaultOptions,
        target: USER_SERVICE_URL,
        pathRewrite: {
          [`^/api/v1/profile`]: `/api/users/${userId}`
        }
      });
      
      profileProxy(req, res, next);
    }
  );
  
  // Notification routes
  router.use(
    '/api/v1/notifications',
    authenticate,
    (req, res, next) => {
      // Use a custom route handler that dynamically creates the proxy with user ID
      const userId = req.user.id;
      const notificationsProxy = createProxyMiddleware({
        ...defaultOptions,
        target: NOTIFICATION_SERVICE_URL,
        pathRewrite: {
          [`^/api/v1/notifications`]: `/api/notifications/user/${userId}`
        }
      });
      
      notificationsProxy(req, res, next);
    }
  );
  
  // Admin routes (admin authorization required)
  // Product management (create, update, delete)
  router.use(
    '/api/v1/admin/products',
    authenticate,
    authorizeAdmin,
    createProxyMiddleware({
      ...defaultOptions,
      target: PRODUCT_SERVICE_URL,
      pathRewrite: {
        '^/api/v1/admin/products': '/api/products'
      }
    })
  );
  
  // Order management
  router.use(
    '/api/v1/admin/orders',
    authenticate,
    authorizeAdmin,
    createProxyMiddleware({
      ...defaultOptions,
      target: ORDER_SERVICE_URL,
      pathRewrite: {
        '^/api/v1/admin/orders': '/api/orders'
      }
    })
  );
  
  // User management
  router.use(
    '/api/v1/admin/users',
    authenticate,
    authorizeAdmin,
    createProxyMiddleware({
      ...defaultOptions,
      target: USER_SERVICE_URL,
      pathRewrite: {
        '^/api/v1/admin/users': '/api/users'
      }
    })
  );
  
  module.exports = router;
                  

api-gateway/src/app.js

  require('dotenv').config();
  const express = require('express');
  const cors = require('cors');
  const morgan = require('morgan');
  const winston = require('winston');
  const proxyRoutes = require('./routes/proxy.routes');
  
  const app = express();
  const PORT = process.env.PORT || 8000;
  
  // Logger configuration
  const logger = winston.createLogger({
    level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
    format: winston.format.combine(
      winston.format.timestamp(),
      winston.format.json()
    ),
    transports: [
      new winston.transports.Console(),
      new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
      new winston.transports.File({ filename: 'logs/combined.log' })
    ]
  });
  
  // Middleware
  app.use(cors());
  app.use(express.json());
  app.use(morgan('dev'));
  
  // Add request logging
  app.use((req, res, next) => {
    logger.info(`${req.method} ${req.url}`, {
      ip: req.ip,
      userAgent: req.get('User-Agent')
    });
    next();
  });
  
  // Register proxy routes
  app.use(proxyRoutes);
  
  // Health check endpoint
  app.get('/health', (req, res) => {
    res.status(200).json({ status: 'ok' });
  });
  
  // Error handling
  app.use((err, req, res, next) => {
    logger.error(`Error processing request: ${err.message}`, {
      stack: err.stack,
      url: req.url,
      method: req.method
    });
    
    res.status(500).json({
      message: 'An error occurred processing your request',
      error: process.env.NODE_ENV === 'production' ? undefined : err.message
    });
  });
  
  // Start server
  app.listen(PORT, () => {
    logger.info(`API Gateway running on port ${PORT}`);
  });
  
  module.exports = app; // For testing
                  

api-gateway/package.json

  {
    "name": "api-gateway",
    "version": "1.0.0",
    "description": "API Gateway for Microservices E-commerce Application",
    "main": "src/app.js",
    "scripts": {
      "start": "node src/app.js",
      "dev": "nodemon src/app.js",
      "test": "jest"
    },
    "dependencies": {
      "express": "^4.18.2",
      "cors": "^2.8.5",
      "dotenv": "^16.3.1",
      "axios": "^1.4.0",
      "http-proxy-middleware": "^2.0.6",
      "winston": "^3.10.0",
      "morgan": "^1.10.0"
    },
    "devDependencies": {
      "jest": "^29.6.2",
      "supertest": "^6.3.3",
      "nodemon": "^3.0.1"
    }
  }
                  

api-gateway/.env

  PORT=8000
  NODE_ENV=development
  PRODUCT_SERVICE_URL=http://product-service:3000
  USER_SERVICE_URL=http://user-service:3001
  CART_SERVICE_URL=http://cart-service:3002
  ORDER_SERVICE_URL=http://order-service:3003
  NOTIFICATION_SERVICE_URL=http://notification-service:3004
                  

api-gateway/Dockerfile

  FROM node:18-alpine
  
  WORKDIR /usr/src/app
  
  COPY package*.json ./
  
  RUN npm install
  
  COPY . .
  
  EXPOSE 8000
  
  CMD ["npm", "start"]
                  

Creating a Simple React Frontend

Let's create a simple React frontend to interact with our microservices through the API Gateway.

client/package.json

  {
    "name": "ecommerce-client",
    "version": "0.1.0",
    "private": true,
    "dependencies": {
      "@testing-library/jest-dom": "^5.17.0",
      "@testing-library/react": "^13.4.0",
      "@testing-library/user-event": "^13.5.0",
      "axios": "^1.4.0",
      "react": "^18.2.0",
      "react-dom": "^18.2.0",
      "react-router-dom": "^6.14.2",
      "react-scripts": "5.0.1",
      "web-vitals": "^2.1.4"
    },
    "scripts": {
      "start": "react-scripts start",
      "build": "react-scripts build",
      "test": "react-scripts test",
      "eject": "react-scripts eject"
    },
    "eslintConfig": {
      "extends": [
        "react-app",
        "react-app/jest"
      ]
    },
    "browserslist": {
      "production": [
        ">0.2%",
        "not dead",
        "not op_mini all"
      ],
      "development": [
        "last 1 chrome version",
        "last 1 firefox version",
        "last 1 safari version"
      ]
    }
  }
                  

client/src/services/api.js

  import axios from 'axios';
  
  const API_URL = process.env.REACT_APP_API_URL || 'http://localhost:8000/api/v1';
  
  // Create axios instance
  const api = axios.create({
    baseURL: API_URL,
    headers: {
      'Content-Type': 'application/json'
    }
  });
  
  // Add token to requests when available
  api.interceptors.request.use(
    (config) => {
      const token = localStorage.getItem('token');
      if (token) {
        config.headers.Authorization = `Bearer ${token}`;
      }
      return config;
    },
    (error) => {
      return Promise.reject(error);
    }
  );
  
  // Handle auth errors
  api.interceptors.response.use(
    (response) => response,
    (error) => {
      if (error.response && error.response.status === 401) {
        localStorage.removeItem('token');
        localStorage.removeItem('user');
        window.location.href = '/login';
      }
      return Promise.reject(error);
    }
  );
  
  // Auth service
  export const authService = {
    login: async (email, password) => {
      const response = await api.post('/auth/login', { email, password });
      if (response.data.token) {
        localStorage.setItem('token', response.data.token);
        localStorage.setItem('user', JSON.stringify(response.data.user));
      }
      return response.data;
    },
    
    register: async (userData) => {
      const response = await api.post('/auth/register', userData);
      if (response.data.token) {
        localStorage.setItem('token', response.data.token);
        localStorage.setItem('user', JSON.stringify(response.data.user));
      }
      return response.data;
    },
    
    logout: () => {
      localStorage.removeItem('token');
      localStorage.removeItem('user');
    },
    
    getCurrentUser: () => {
      const user = localStorage.getItem('user');
      return user ? JSON.parse(user) : null;
    }
  };
  
  // Product service
  export const productService = {
    getProducts: async (query = '') => {
      const response = await api.get(`/products${query}`);
      return response.data;
    },
    
    getProductById: async (id) => {
      const response = await api.get(`/products/${id}`);
      return response.data;
    }
  };
  
  // Cart service
  export const cartService = {
    getCart: async () => {
      const response = await api.get('/cart');
      return response.data;
    },
    
    addToCart: async (productId, quantity) => {
      const response = await api.post('/cart/items', { productId, quantity });
      return response.data;
    },
    
    updateQuantity: async (productId, quantity) => {
      const response = await api.put(`/cart/items/${productId}`, { quantity });
      return response.data;
    },
    
    removeItem: async (productId) => {
      const response = await api.delete(`/cart/items/${productId}`);
      return response.data;
    },
    
    clearCart: async () => {
      const response = await api.delete('/cart');
      return response.data;
    }
  };
  
  // Order service
  export const orderService = {
    createOrder: async (paymentMethod) => {
      const response = await api.post('/orders', { paymentMethod });
      return response.data;
    },
    
    getUserOrders: async () => {
      const response = await api.get('/orders');
      return response.data;
    },
    
    getOrderById: async (id) => {
      const response = await api.get(`/orders/${id}`);
      return response.data;
    },
    
    processPayment: async (orderId, paymentDetails) => {
      const response = await api.post(`/orders/${orderId}/payment`, { paymentDetails });
      return response.data;
    }
  };
  
  // Profile service
  export const profileService = {
    getProfile: async () => {
      const response = await api.get('/profile');
      return response.data;
    },
    
    updateProfile: async (profileData) => {
      const response = await api.put('/profile', profileData);
      return response.data;
    }
  };
  
  // Notification service
  export const notificationService = {
    getNotifications: async () => {
      const response = await api.get('/notifications');
      return response.data;
    },
    
    markAsRead: async (notificationId) => {
      const response = await api.put(`/notifications/${notificationId}/read`);
      return response.data;
    }
  };
                  

client/src/App.js

  import React, { useState, useEffect } from 'react';
  import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
  import { authService } from './services/api';
  
  // Import components
  import Header from './components/Header';
  import Footer from './components/Footer';
  import HomePage from './pages/HomePage';
  import LoginPage from './pages/LoginPage';
  import RegisterPage from './pages/RegisterPage';
  import ProductsPage from './pages/ProductsPage';
  import ProductDetailPage from './pages/ProductDetailPage';
  import CartPage from './pages/CartPage';
  import CheckoutPage from './pages/CheckoutPage';
  import OrdersPage from './pages/OrdersPage';
  import OrderDetailPage from './pages/OrderDetailPage';
  import ProfilePage from './pages/ProfilePage';
  import NotificationsPage from './pages/NotificationsPage';
  import NotFoundPage from './pages/NotFoundPage';
  
  // Protected route component
  const ProtectedRoute = ({ children }) => {
    const user = authService.getCurrentUser();
    
    if (!user) {
      return <Navigate to="/login" replace />;
    }
    
    return children;
  };
  
  function App() {
    const [user, setUser] = useState(null);
    
    useEffect(() => {
      // Check if user is logged in
      const currentUser = authService.getCurrentUser();
      setUser(currentUser);
    }, []);
    
    const handleLogin = (userData) => {
      setUser(userData);
    };
    
    const handleLogout = () => {
      authService.logout();
      setUser(null);
    };
    
    return (
      <Router>
        <div className="app">
          <Header user={user} onLogout={handleLogout} />
          <main className="main-content">
            <Routes>
              <Route path="/" element={<HomePage />} />
              <Route path="/products" element={<ProductsPage />} />
              <Route path="/products/:id" element={<ProductDetailPage />} />
              <Route path="/login" element={<LoginPage onLogin={handleLogin} />} />
              <Route path="/register" element={<RegisterPage onLogin={handleLogin} />} />
              
              {/* Protected routes */}
              <Route path="/cart" element={
                <ProtectedRoute>
                  <CartPage />
                </ProtectedRoute>
              } />
              <Route path="/checkout" element={
                <ProtectedRoute>
                  <CheckoutPage />
                </ProtectedRoute>
              } />
              <Route path="/orders" element={
                <ProtectedRoute>
                  <OrdersPage />
                </ProtectedRoute>
              } />
              <Route path="/orders/:id" element={
                <ProtectedRoute>
                  <OrderDetailPage />
                </ProtectedRoute>
              } />
              <Route path="/profile" element={
                <ProtectedRoute>
                  <ProfilePage user={user} setUser={setUser} />
                </ProtectedRoute>
              } />
              <Route path="/notifications" element={
                <ProtectedRoute>
                  <NotificationsPage />
                </ProtectedRoute>
              } />
              
              {/* 404 Page */}
              <Route path="*" element={<NotFoundPage />} />
            </Routes>
          </main>
          <Footer />
        </div>
      </Router>
    );
  }
  
  export default App;
                  

client/src/components/Header.js

  import React from 'react';
  import { Link } from 'react-router-dom';
  
  const Header = ({ user, onLogout }) => {
    return (
      <header className="header">
        <div className="container">
          <div className="logo">
            <Link to="/">MicroStore</Link>
          </div>
          
          <nav className="nav">
            <ul>
              <li><Link to="/">Home</Link></li>
              <li><Link to="/products">Products</Link></li>
              
              {user ? (
                <>
                  <li><Link to="/cart">Cart</Link></li>
                  <li><Link to="/orders">Orders</Link></li>
                  <li><Link to="/notifications">Notifications</Link></li>
                  <li><Link to="/profile">Profile</Link></li>
                  <li><button onClick={onLogout}>Logout</button></li>
                </>
              ) : (
                <>
                  <li><Link to="/login">Login</Link></li>
                  <li><Link to="/register">Register</Link></li>
                </>
              )}
            </ul>
          </nav>
        </div>
      </header>
    );
  };
  
  export default Header;
                  

client/src/pages/ProductsPage.js

  import React, { useState, useEffect } from 'react';
  import { Link } from 'react-router-dom';
  import { productService } from '../services/api';
  
  const ProductsPage = () => {
    const [products, setProducts] = useState([]);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [category, setCategory] = useState('');
    const [categories, setCategories] = useState([]);
  
    useEffect(() => {
      const fetchProducts = async () => {
        try {
          setLoading(true);
          const query = category ? `?category=${category}` : '';
          const data = await productService.getProducts(query);
          setProducts(data);
          
          // Extract unique categories
          const uniqueCategories = [...new Set(data.map(product => product.category))];
          setCategories(uniqueCategories);
          
          setLoading(false);
        } catch (error) {
          setError(error.message);
          setLoading(false);
        }
      };
  
      fetchProducts();
    }, [category]);
  
    const handleCategoryChange = (e) => {
      setCategory(e.target.value);
    };
  
    if (loading) {
      return <div className="loading">Loading products...</div>;
    }
  
    if (error) {
      return <div className="error">Error: {error}</div>;
    }
  
    return (
      <div className="products-page">
        <h1>Products</h1>
        
        <div className="filters">
          <select 
            value={category} 
            onChange={handleCategoryChange}
            className="category-filter"
          >
            <option value="">All Categories</option>
            {categories.map(cat => (
              <option key={cat} value={cat}>{cat}</option>
            ))}
          </select>
        </div>
        
        <div className="products-grid">
          {products.length === 0 ? (
            <p>No products found.</p>
          ) : (
            products.map(product => (
              <div key={product._id} className="product-card">
                <img 
                  src={product.imageUrl || '/default-product.jpg'} 
                  alt={product.name} 
                  className="product-image"
                />
                <div className="product-info">
                  <h3>{product.name}</h3>
                  <p className="product-price">${product.price.toFixed(2)}</p>
                  <p className="product-category">{product.category}</p>
                  <Link to={`/products/${product._id}`} className="view-button">
                    View Details
                  </Link>
                </div>
              </div>
            ))
          )}
        </div>
      </div>
    );
  };
  
  export default ProductsPage;
                  

client/src/pages/ProductDetailPage.js

  import React, { useState, useEffect } from 'react';
  import { useParams, useNavigate } from 'react-router-dom';
  import { productService, cartService, authService } from '../services/api';
  
  const ProductDetailPage = () => {
    const { id } = useParams();
    const navigate = useNavigate();
    const [product, setProduct] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState(null);
    const [quantity, setQuantity] = useState(1);
    const [addingToCart, setAddingToCart] = useState(false);
    const [message, setMessage] = useState(null);
    
    const user = authService.getCurrentUser();
  
    useEffect(() => {
      const fetchProduct = async () => {
        try {
          setLoading(true);
          const data = await productService.getProductById(id);
          setProduct(data);
          setLoading(false);
        } catch (error) {
          setError(error.message);
          setLoading(false);
        }
      };
  
      fetchProduct();
    }, [id]);
  
    const handleQuantityChange = (e) => {
      const value = parseInt(e.target.value);
      setQuantity(Math.max(1, Math.min(value, product.inventory)));
    };
  
    const handleAddToCart = async () => {
      if (!user) {
        navigate('/login');
        return;
      }
      
      try {
        setAddingToCart(true);
        await cartService.addToCart(product._id, quantity);
        setMessage('Product added to cart!');
        setTimeout(() => setMessage(null), 3000);
        setAddingToCart(false);
      } catch (error) {
        setError(error.response?.data?.message || error.message);
        setAddingToCart(false);
      }
    };
  
    if (loading) {
      return <div className="loading">Loading product details...</div>;
    }
  
    if (error) {
      return <div className="error">Error: {error}</div>;
    }
  
    if (!product) {
      return <div className="error">Product not found.</div>;
    }
  
    return (
      <div className="product-detail-page">
        {message && (
          <div className="success-message">{message}</div>
        )}
        
        <div className="product-detail">
          <div className="product-image-container">
            <img 
              src={product.imageUrl || '/default-product.jpg'} 
              alt={product.name} 
              className="product-detail-image"
            />
          </div>
          
          <div className="product-info">
            <h1>{product.name}</h1>
            <p className="product-price">${product.price.toFixed(2)}</p>
            <p className="product-category">Category: {product.category}</p>
            <p className="product-inventory">
              {product.inventory > 0 ? (
                `In Stock: ${product.inventory} units`
              ) : (
                <span className="out-of-stock">Out of Stock</span>
              )}
            </p>
            
            <div className="product-description">
              <h2>Description</h2>
              <p>{product.description}</p>
            </div>
            
            {product.inventory > 0 && (
              <div className="add-to-cart">
                <div className="quantity-control">
                  <label htmlFor="quantity">Quantity:</label>
                  <input
                    type="number"
                    id="quantity"
                    value={quantity}
                    onChange={handleQuantityChange}
                    min="1"
                    max={product.inventory}
                  />
                </div>
                
                <button 
                  onClick={handleAddToCart} 
                  disabled={addingToCart}
                  className="add-to-cart-button"
                >
                  {addingToCart ? 'Adding...' : 'Add to Cart'}
                </button>
              </div>
            )}
          </div>
        </div>
      </div>
    );
  };
  
  export default ProductDetailPage;
                  

Docker and Docker Compose Setup

To deploy our microservices application, we'll use Docker and Docker Compose to containerize each service.

docker-compose.yml

  version: '3.8'
  
  services:
    # API Gateway
    api-gateway:
      build: ./api-gateway
      ports:
        - "8000:8000"
      environment:
        - PORT=8000
        - NODE_ENV=development
        - PRODUCT_SERVICE_URL=http://product-service:3000
        - USER_SERVICE_URL=http://user-service:3001
        - CART_SERVICE_URL=http://cart-service:3002
        - ORDER_SERVICE_URL=http://order-service:3003
        - NOTIFICATION_SERVICE_URL=http://notification-service:3004
      depends_on:
        - product-service
        - user-service
        - cart-service
        - order-service
        - notification-service
      networks:
        - microservices-network
      restart: unless-stopped
  
    # Product Service
    product-service:
      build: ./product-service
      ports:
        - "3000:3000"
      environment:
        - PORT=3000
        - MONGODB_URI=mongodb://mongodb:27017/product-service
        - NODE_ENV=development
      depends_on:
        - mongodb
      networks:
        - microservices-network
      restart: unless-stopped
  
    # User Service
    user-service:
      build: ./user-service
      ports:
        - "3001:3001"
      environment:
        - PORT=3001
        - MONGODB_URI=mongodb://mongodb:27017/user-service
        - JWT_SECRET=your_jwt_secret_key
        - NODE_ENV=development
      depends_on:
        - mongodb
      networks:
        - microservices-network
      restart: unless-stopped
  
    # Cart Service
    cart-service:
      build: ./cart-service
      ports:
        - "3002:3002"
      environment:
        - PORT=3002
        - MONGODB_URI=mongodb://mongodb:27017/cart-service
        - NODE_ENV=development
        - PRODUCT_SERVICE_URL=http://product-service:3000/api/products
      depends_on:
        - mongodb
        - product-service
      networks:
        - microservices-network
      restart: unless-stopped
  
    # Order Service
    order-service:
      build: ./order-service
      ports:
        - "3003:3003"
      environment:
        - PORT=3003
        - MONGODB_URI=mongodb://mongodb:27017/order-service
        - NODE_ENV=development
        - CART_SERVICE_URL=http://cart-service:3002/api/carts
        - PRODUCT_SERVICE_URL=http://product-service:3000/api/products
        - USER_SERVICE_URL=http://user-service:3001/api/users
        - RABBITMQ_URL=amqp://rabbitmq:5672
      depends_on:
        - mongodb
        - rabbitmq
        - product-service
        - cart-service
        - user-service
      networks:
        - microservices-network
      restart: unless-stopped
  
    # Notification Service
    notification-service:
      build: ./notification-service
      ports:
        - "3004:3004"
      environment:
        - PORT=3004
        - MONGODB_URI=mongodb://mongodb:27017/notification-service
        - NODE_ENV=development
        - USER_SERVICE_URL=http://user-service:3001/api/users
        - RABBITMQ_URL=amqp://rabbitmq:5672
      depends_on:
        - mongodb
        - rabbitmq
        - user-service
      networks:
        - microservices-network
      restart: unless-stopped
  
    # Frontend Client
    client:
      build: ./client
      ports:
        - "3005:3000"
      environment:
        - REACT_APP_API_URL=http://localhost:8000/api/v1
      depends_on:
        - api-gateway
      networks:
        - microservices-network
      restart: unless-stopped
  
    # MongoDB
    mongodb:
      image: mongo:latest
      ports:
        - "27017:27017"
      volumes:
        - mongodb-data:/data/db
      networks:
        - microservices-network
      restart: unless-stopped
  
    # RabbitMQ
    rabbitmq:
      image: rabbitmq:3-management
      ports:
        - "5672:5672"
        - "15672:15672"
      volumes:
        - rabbitmq-data:/var/lib/rabbitmq
      networks:
        - microservices-network
      restart: unless-stopped
  
  networks:
    microservices-network:
      driver: bridge
  
  volumes:
    mongodb-data:
    rabbitmq-data:
                  

Running the Application

Now that we have implemented all the services, let's see how to run the application using Docker Compose.

Step 1: Build and Start the Services

  # Navigate to the project root directory
  cd microservices-ecommerce
  
  # Build and start all services
  docker-compose up --build

This command will build all Docker images and start the containers. The first build might take some time.

Step 2: Access the Application

Step 3: Test the APIs

You can use tools like Postman or curl to test the API endpoints through the API Gateway.

Example: Register a User

  curl -X POST http://localhost:8000/api/v1/auth/register \
    -H "Content-Type: application/json" \
    -d '{
      "username": "testuser",
      "email": "test@example.com",
      "password": "password123",
      "name": "Test User",
      "address": {
        "street": "123 Test St",
        "city": "Test City",
        "state": "Test State",
        "zipCode": "12345",
        "country": "Test Country"
      }
    }'

Example: Login

  curl -X POST http://localhost:8000/api/v1/auth/login \
    -H "Content-Type: application/json" \
    -d '{
      "email": "test@example.com",
      "password": "password123"
    }'

Example: Get Products

  curl -X GET http://localhost:8000/api/v1/products

Example: Add Product to Cart (Authenticated)

  curl -X POST http://localhost:8000/api/v1/cart/items \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer YOUR_TOKEN_HERE" \
    -d '{
      "productId": "PRODUCT_ID_HERE",
      "quantity": 1
    }'

Step 4: Evaluate the Solution

After implementing and running our microservices application, let's reflect on what we've accomplished and potential improvements.

What We've Accomplished

Key Benefits of Our Microservices Architecture

graph TD A[Microservices Architecture] --> B[Independent Deployment] A --> C[Technology Flexibility] A --> D[Fault Isolation] A --> E[Scalability] A --> F[Team Autonomy] B --> G[Faster Releases] C --> H[Right Tool for Each Job] D --> I[System Resilience] E --> J[Resource Optimization] F --> K[Parallel Development]

Challenges and Improvements

Conclusion

In this weekend project, we've built a comprehensive microservices-based e-commerce application that demonstrates the key concepts and patterns of microservices architecture.

We've seen how to:

This project serves as a foundation that you can extend with additional features, improved resilience, and more sophisticated deployment strategies as you continue your journey into microservices architecture.

Next Steps for Learning

  • Explore service mesh technologies like Istio
  • Implement Kubernetes for container orchestration
  • Learn about event sourcing and CQRS patterns
  • Explore continuous deployment pipelines for microservices
  • Study domain-driven design for better service boundaries

Additional Resources