Skip to content

Microservices Architecture

Building a scalable microservices architecture with Response Handler for consistent API responses across services.

Architecture Overview

Service Structure

microservices/
├── api-gateway/          # Entry point, routing & auth
├── user-service/         # User management
├── product-service/      # Product catalog
├── order-service/        # Order processing
├── notification-service/ # Notifications
└── shared/              # Shared utilities
    ├── response-handler/ # Common response patterns
    └── service-client/   # Inter-service communication

API Gateway

Gateway Implementation

javascript
// api-gateway/server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const { quickSetup } = require('response-handler');

const app = express();

// Response Handler for gateway
app.use(
  quickSetup({
    enableLogging: true,
    logLevel: 'info',
    environment: process.env.NODE_ENV || 'development',
    responseHeaders: {
      'X-Gateway': 'microservices-v1',
      'X-Request-ID': true,
    },
  }),
);

// Service registry
const services = {
  user: process.env.USER_SERVICE_URL || 'http://localhost:3001',
  product: process.env.PRODUCT_SERVICE_URL || 'http://localhost:3002',
  order: process.env.ORDER_SERVICE_URL || 'http://localhost:3003',
  notification: process.env.NOTIFICATION_SERVICE_URL || 'http://localhost:3004',
};

// Authentication middleware
const authenticateRequest = async (req, res, next) => {
  const token = req.headers.authorization;

  if (!token) {
    return res.unauthorized({}, 'Authentication token required');
  }

  try {
    const response = await fetch(`${services.user}/auth/verify`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        Authorization: token,
      },
    });

    if (!response.ok) {
      return res.unauthorized({}, 'Invalid authentication token');
    }

    const userData = await response.json();
    req.user = userData.data;
    next();
  } catch (error) {
    res.badGateway(
      { service: 'user-service', error: error.message },
      'Authentication service unavailable',
    );
  }
};

// Proxy middleware with error handling
const createServiceProxy = (serviceName, serviceUrl) => {
  return createProxyMiddleware({
    target: serviceUrl,
    changeOrigin: true,
    pathRewrite: {
      [`^/api/${serviceName}`]: '',
    },
    onError: (err, req, res) => {
      console.error(`${serviceName} service error:`, err);
      res.badGateway(
        {
          service: serviceName,
          error: err.message,
          timestamp: new Date(),
        },
        `${serviceName} service unavailable`,
      );
    },
    onProxyReq: (proxyReq, req) => {
      // Forward user context
      if (req.user) {
        proxyReq.setHeader('X-User-ID', req.user.id);
        proxyReq.setHeader('X-User-Role', req.user.role);
        proxyReq.setHeader('X-User-Context', JSON.stringify(req.user));
      }
    },
  });
};

// Service routes
app.use('/api/users', createServiceProxy('user', services.user));
app.use('/api/products', createServiceProxy('product', services.product));
app.use('/api/orders', authenticateRequest, createServiceProxy('order', services.order));
app.use(
  '/api/notifications',
  authenticateRequest,
  createServiceProxy('notification', services.notification),
);

// Health check endpoint
app.get('/health', async (req, res) => {
  const healthChecks = {};

  for (const [serviceName, serviceUrl] of Object.entries(services)) {
    try {
      const response = await fetch(`${serviceUrl}/health`, { timeout: 5000 });
      healthChecks[serviceName] = {
        status: response.ok ? 'healthy' : 'unhealthy',
        responseTime: response.headers.get('x-response-time') || 'unknown',
      };
    } catch (error) {
      healthChecks[serviceName] = {
        status: 'unhealthy',
        error: error.message,
      };
    }
  }

  const allHealthy = Object.values(healthChecks).every((check) => check.status === 'healthy');

  if (allHealthy) {
    res.ok(healthChecks, 'All services healthy');
  } else {
    res.serviceUnavailable(healthChecks, 'Some services are unhealthy');
  }
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`API Gateway running on port ${PORT}`);
});

User Service

User Management Microservice

javascript
// user-service/server.js
const express = require('express');
const { quickSetup } = require('response-handler');
const { Pool } = require('pg');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');

const app = express();

// Database connection
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: process.env.NODE_ENV === 'production',
});

// Middleware
app.use(express.json());
app.use(
  quickSetup({
    enableLogging: true,
    logLevel: 'info',
    environment: process.env.NODE_ENV || 'development',
    responseHeaders: {
      'X-Service': 'user-service',
      'X-Version': '1.0.0',
    },
  }),
);

// Extract user context from gateway
app.use((req, res, next) => {
  const userContext = req.headers['x-user-context'];
  if (userContext) {
    try {
      req.user = JSON.parse(userContext);
    } catch (error) {
      console.warn('Invalid user context:', error);
    }
  }
  next();
});

// Register user
app.post('/register', async (req, res) => {
  try {
    const { email, password, name } = req.body;

    // Validation
    if (!email || !password || !name) {
      return res.badRequest(
        { missingFields: ['email', 'password', 'name'].filter((field) => !req.body[field]) },
        'Missing required fields',
      );
    }

    // Check if user exists
    const existingUser = await pool.query('SELECT id FROM users WHERE email = $1', [email]);

    if (existingUser.rows.length > 0) {
      return res.conflict({ email }, 'User already exists');
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, 12);

    // Create user
    const result = await pool.query(
      'INSERT INTO users (email, name, password_hash, created_at) VALUES ($1, $2, $3, NOW()) RETURNING id, email, name, created_at',
      [email, name, hashedPassword],
    );

    const user = result.rows[0];

    // Generate token
    const token = jwt.sign(
      { id: user.id, email: user.email, name: user.name },
      process.env.JWT_SECRET || 'secret',
      { expiresIn: '24h' },
    );

    // Publish user created event
    await publishEvent('user.created', { userId: user.id, email: user.email });

    res.created({ user, token }, 'User registered successfully');
  } catch (error) {
    res.error(error, 'Registration failed');
  }
});

// Login user
app.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    if (!email || !password) {
      return res.badRequest(
        { missingFields: ['email', 'password'].filter((field) => !req.body[field]) },
        'Email and password required',
      );
    }

    // Find user
    const result = await pool.query(
      'SELECT id, email, name, password_hash FROM users WHERE email = $1',
      [email],
    );

    if (result.rows.length === 0) {
      return res.unauthorized({}, 'Invalid credentials');
    }

    const user = result.rows[0];

    // Verify password
    const isValidPassword = await bcrypt.compare(password, user.password_hash);
    if (!isValidPassword) {
      return res.unauthorized({}, 'Invalid credentials');
    }

    // Generate token
    const token = jwt.sign(
      { id: user.id, email: user.email, name: user.name },
      process.env.JWT_SECRET || 'secret',
      { expiresIn: '24h' },
    );

    res.ok(
      {
        user: {
          id: user.id,
          email: user.email,
          name: user.name,
        },
        token,
      },
      'Login successful',
    );
  } catch (error) {
    res.error(error, 'Login failed');
  }
});

// Verify token (for gateway)
app.post('/auth/verify', (req, res) => {
  try {
    const token = req.headers.authorization?.split(' ')[1];

    if (!token) {
      return res.unauthorized({}, 'Token required');
    }

    const decoded = jwt.verify(token, process.env.JWT_SECRET || 'secret');

    res.ok(
      {
        id: decoded.id,
        email: decoded.email,
        name: decoded.name,
      },
      'Token valid',
    );
  } catch (error) {
    res.unauthorized({ error: error.message }, 'Invalid token');
  }
});

// Get users
app.get('/users', async (req, res) => {
  try {
    const { page = 1, limit = 10 } = req.query;
    const offset = (page - 1) * limit;

    const result = await pool.query(
      'SELECT id, email, name, created_at FROM users ORDER BY created_at DESC LIMIT $1 OFFSET $2',
      [limit, offset],
    );

    const countResult = await pool.query('SELECT COUNT(*) FROM users');
    const total = parseInt(countResult.rows[0].count);

    res.ok(result.rows, 'Users retrieved successfully', {
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total,
        totalPages: Math.ceil(total / limit),
      },
    });
  } catch (error) {
    res.error(error, 'Failed to retrieve users');
  }
});

// Event publishing function
async function publishEvent(eventType, data) {
  try {
    console.log(`Publishing event: ${eventType}`, data);
    // Implement actual event publishing (Redis, RabbitMQ, etc.)
  } catch (error) {
    console.error('Failed to publish event:', error);
  }
}

// Health check
app.get('/health', async (req, res) => {
  try {
    await pool.query('SELECT 1');
    res.ok(
      {
        service: 'user-service',
        status: 'healthy',
        database: 'connected',
        timestamp: new Date(),
      },
      'Service is healthy',
    );
  } catch (error) {
    res.serviceUnavailable(
      {
        service: 'user-service',
        status: 'unhealthy',
        database: 'disconnected',
        error: error.message,
      },
      'Service is unhealthy',
    );
  }
});

const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
  console.log(`User service running on port ${PORT}`);
});

Product Service

Product Catalog Microservice

javascript
// product-service/server.js
const express = require('express');
const { quickSetup } = require('response-handler');
const { MongoClient } = require('mongodb');

const app = express();

// MongoDB connection
let db;
MongoClient.connect(process.env.MONGODB_URL || 'mongodb://localhost:27017/products').then(
  (client) => {
    db = client.db();
    console.log('Connected to MongoDB');
  },
);

// Middleware
app.use(express.json());
app.use(
  quickSetup({
    enableLogging: true,
    logLevel: 'info',
    environment: process.env.NODE_ENV || 'development',
    responseHeaders: {
      'X-Service': 'product-service',
      'X-Version': '1.0.0',
    },
  }),
);

// Get products
app.get('/products', async (req, res) => {
  try {
    const { page = 1, limit = 20, category, minPrice, maxPrice, search } = req.query;

    const skip = (page - 1) * limit;
    const query = {};

    if (category) query.category = category;
    if (minPrice || maxPrice) {
      query.price = {};
      if (minPrice) query.price.$gte = parseFloat(minPrice);
      if (maxPrice) query.price.$lte = parseFloat(maxPrice);
    }
    if (search) query.$text = { $search: search };

    const products = await db
      .collection('products')
      .find(query)
      .skip(skip)
      .limit(parseInt(limit))
      .toArray();

    const total = await db.collection('products').countDocuments(query);

    res.ok(products, 'Products retrieved successfully', {
      pagination: {
        page: parseInt(page),
        limit: parseInt(limit),
        total,
        totalPages: Math.ceil(total / limit),
      },
    });
  } catch (error) {
    res.error(error, 'Failed to retrieve products');
  }
});

// Get single product
app.get('/products/:id', async (req, res) => {
  try {
    const { id } = req.params;

    const product = await db.collection('products').findOne({ _id: id });

    if (!product) {
      return res.notFound({ productId: id }, 'Product not found');
    }

    res.ok(product, 'Product retrieved successfully');
  } catch (error) {
    res.error(error, 'Failed to retrieve product');
  }
});

// Create product
app.post('/products', async (req, res) => {
  try {
    const { name, description, price, category, stock } = req.body;

    // Validation
    if (!name || !price || !category) {
      return res.badRequest(
        { missingFields: ['name', 'price', 'category'] },
        'Missing required fields',
      );
    }

    if (price <= 0) {
      return res.badRequest(
        { price, requirement: 'greater than 0' },
        'Price must be greater than 0',
      );
    }

    const product = {
      _id: generateProductId(),
      name: name.trim(),
      description: description?.trim() || '',
      price: parseFloat(price),
      category,
      stock: parseInt(stock) || 0,
      createdAt: new Date(),
      isActive: true,
    };

    await db.collection('products').insertOne(product);

    // Publish product created event
    await publishEvent('product.created', {
      productId: product._id,
      name: product.name,
      category: product.category,
    });

    res.created(product, 'Product created successfully');
  } catch (error) {
    res.error(error, 'Failed to create product');
  }
});

// Update stock
app.patch('/products/:id/stock', async (req, res) => {
  try {
    const { id } = req.params;
    const { quantity, operation = 'decrease' } = req.body;

    const product = await db.collection('products').findOne({ _id: id });
    if (!product) {
      return res.notFound({ productId: id }, 'Product not found');
    }

    const stockChange = operation === 'increase' ? quantity : -quantity;
    const newStock = product.stock + stockChange;

    if (newStock < 0) {
      return res.badRequest(
        { currentStock: product.stock, requestedChange: stockChange },
        'Insufficient stock',
      );
    }

    await db
      .collection('products')
      .updateOne({ _id: id }, { $set: { stock: newStock, updatedAt: new Date() } });

    res.ok(
      {
        productId: id,
        previousStock: product.stock,
        newStock,
      },
      'Stock updated successfully',
    );
  } catch (error) {
    res.error(error, 'Failed to update stock');
  }
});

function generateProductId() {
  return Date.now().toString(36) + Math.random().toString(36).substr(2);
}

async function publishEvent(eventType, data) {
  console.log(`Publishing event: ${eventType}`, data);
}

// Health check
app.get('/health', async (req, res) => {
  try {
    await db.admin().ping();
    res.ok(
      {
        service: 'product-service',
        status: 'healthy',
        database: 'connected',
      },
      'Service is healthy',
    );
  } catch (error) {
    res.serviceUnavailable(
      {
        service: 'product-service',
        status: 'unhealthy',
        error: error.message,
      },
      'Service is unhealthy',
    );
  }
});

const PORT = process.env.PORT || 3002;
app.listen(PORT, () => {
  console.log(`Product service running on port ${PORT}`);
});

Order Service

Order Processing Microservice

javascript
// order-service/server.js
const express = require('express');
const { quickSetup } = require('response-handler');
const { Pool } = require('pg');

const app = express();

// Database and service connections
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  ssl: process.env.NODE_ENV === 'production',
});

const services = {
  product: process.env.PRODUCT_SERVICE_URL || 'http://localhost:3002',
  user: process.env.USER_SERVICE_URL || 'http://localhost:3001',
};

// Middleware
app.use(express.json());
app.use(
  quickSetup({
    enableLogging: true,
    logLevel: 'info',
    environment: process.env.NODE_ENV || 'development',
    responseHeaders: {
      'X-Service': 'order-service',
      'X-Version': '1.0.0',
    },
  }),
);

// Extract user context
app.use((req, res, next) => {
  const userContext = req.headers['x-user-context'];
  if (userContext) {
    try {
      req.user = JSON.parse(userContext);
    } catch (error) {
      console.warn('Invalid user context:', error);
    }
  }
  next();
});

// Create order
app.post('/orders', async (req, res) => {
  const client = await pool.connect();

  try {
    const { items, shippingAddress } = req.body;

    if (!req.user) {
      return res.unauthorized({}, 'Authentication required');
    }

    if (!items || !Array.isArray(items) || items.length === 0) {
      return res.badRequest({ items: 'Must be non-empty array' }, 'Order must contain items');
    }

    await client.query('BEGIN');

    // Validate products and calculate total
    let totalAmount = 0;
    const validatedItems = [];

    for (const item of items) {
      const { productId, quantity } = item;

      if (!productId || !quantity || quantity <= 0) {
        await client.query('ROLLBACK');
        return res.badRequest({ item }, 'Invalid item format');
      }

      // Check product
      const productResponse = await fetch(`${services.product}/products/${productId}`);

      if (!productResponse.ok) {
        await client.query('ROLLBACK');
        return res.badRequest({ productId }, 'Product not found');
      }

      const productData = await productResponse.json();
      const product = productData.data;

      if (product.stock < quantity) {
        await client.query('ROLLBACK');
        return res.badRequest(
          { productId, requested: quantity, available: product.stock },
          'Insufficient stock',
        );
      }

      const itemTotal = product.price * quantity;
      totalAmount += itemTotal;

      validatedItems.push({
        productId,
        productName: product.name,
        quantity,
        unitPrice: product.price,
        totalPrice: itemTotal,
      });
    }

    // Create order
    const orderResult = await client.query(
      `
      INSERT INTO orders (user_id, total_amount, status, shipping_address, created_at)
      VALUES ($1, $2, 'pending', $3, NOW())
      RETURNING id, created_at
    `,
      [req.user.id, totalAmount, JSON.stringify(shippingAddress)],
    );

    const order = orderResult.rows[0];

    // Create order items
    for (const item of validatedItems) {
      await client.query(
        `
        INSERT INTO order_items (order_id, product_id, product_name, quantity, unit_price, total_price)
        VALUES ($1, $2, $3, $4, $5, $6)
      `,
        [
          order.id,
          item.productId,
          item.productName,
          item.quantity,
          item.unitPrice,
          item.totalPrice,
        ],
      );
    }

    // Update product stock
    for (const item of validatedItems) {
      await fetch(`${services.product}/products/${item.productId}/stock`, {
        method: 'PATCH',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          quantity: item.quantity,
          operation: 'decrease',
        }),
      });
    }

    await client.query('COMMIT');

    // Publish order created event
    await publishEvent('order.created', {
      orderId: order.id,
      userId: req.user.id,
      totalAmount,
    });

    res.created(
      {
        orderId: order.id,
        totalAmount,
        items: validatedItems,
        status: 'pending',
      },
      'Order created successfully',
    );
  } catch (error) {
    await client.query('ROLLBACK');
    res.error(error, 'Failed to create order');
  } finally {
    client.release();
  }
});

// Get user orders
app.get('/orders', async (req, res) => {
  try {
    if (!req.user) {
      return res.unauthorized({}, 'Authentication required');
    }

    const result = await pool.query(
      'SELECT * FROM orders WHERE user_id = $1 ORDER BY created_at DESC',
      [req.user.id],
    );

    res.ok(result.rows, 'Orders retrieved successfully');
  } catch (error) {
    res.error(error, 'Failed to retrieve orders');
  }
});

async function publishEvent(eventType, data) {
  console.log(`Publishing event: ${eventType}`, data);
}

// Health check
app.get('/health', async (req, res) => {
  try {
    await pool.query('SELECT 1');
    res.ok(
      {
        service: 'order-service',
        status: 'healthy',
        database: 'connected',
      },
      'Service is healthy',
    );
  } catch (error) {
    res.serviceUnavailable(
      {
        service: 'order-service',
        status: 'unhealthy',
        error: error.message,
      },
      'Service is unhealthy',
    );
  }
});

const PORT = process.env.PORT || 3003;
app.listen(PORT, () => {
  console.log(`Order service running on port ${PORT}`);
});

Docker Deployment

Docker Compose Configuration

yaml
# docker-compose.yml
version: '3.8'

services:
  # API Gateway
  api-gateway:
    build: ./api-gateway
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
      - USER_SERVICE_URL=http://user-service:3001
      - PRODUCT_SERVICE_URL=http://product-service:3002
      - ORDER_SERVICE_URL=http://order-service:3003
    depends_on:
      - user-service
      - product-service
      - order-service
    networks:
      - microservices

  # User Service
  user-service:
    build: ./user-service
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@user-db:5432/users
      - JWT_SECRET=your-secret-key
    depends_on:
      - user-db
    networks:
      - microservices

  # Product Service
  product-service:
    build: ./product-service
    environment:
      - NODE_ENV=production
      - MONGODB_URL=mongodb://product-db:27017/products
    depends_on:
      - product-db
    networks:
      - microservices

  # Order Service
  order-service:
    build: ./order-service
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@order-db:5432/orders
      - PRODUCT_SERVICE_URL=http://product-service:3002
      - USER_SERVICE_URL=http://user-service:3001
    depends_on:
      - order-db
    networks:
      - microservices

  # Databases
  user-db:
    image: postgres:13
    environment:
      - POSTGRES_DB=users
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - user-data:/var/lib/postgresql/data
    networks:
      - microservices

  product-db:
    image: mongo:4.4
    volumes:
      - product-data:/data/db
    networks:
      - microservices

  order-db:
    image: postgres:13
    environment:
      - POSTGRES_DB=orders
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
    volumes:
      - order-data:/var/lib/postgresql/data
    networks:
      - microservices

volumes:
  user-data:
  product-data:
  order-data:

networks:
  microservices:
    driver: bridge

This microservices architecture provides:

  • Unified Response Format: All services use Response Handler for consistent API responses
  • Service Discovery: API Gateway routes requests to appropriate services
  • Inter-Service Communication: Services communicate through HTTP APIs with proper error handling
  • Independent Scaling: Each service can be scaled independently
  • Database Per Service: Each service owns its data
  • Event-Driven Architecture: Services publish events for loose coupling
  • Health Monitoring: Each service provides health check endpoints
  • Production Ready: Includes Docker configuration for deployment

The architecture demonstrates how Response Handler ensures consistent response patterns across a distributed system while maintaining service independence.

Released under the ISC License.