Complete Guide to Building Microservices with Node.js and Docker

On this page

Table of contents

    Introduction to Microservices Architecture

    Microservices architecture has revolutionized how we build and deploy modern applications. Instead of monolithic applications, we break down functionality into smaller, independent services that communicate through well-defined APIs. This guide will walk you through building a complete microservices ecosystem using Node.js and Docker.

    Why Microservices?

    Before diving into implementation, let’s understand the benefits and challenges of microservices:

    Aspect Monolithic Microservices
    Deployment Single deployment unit Independent service deployment
    Scaling Scale entire application Scale individual services
    Technology Stack Single stack Polyglot architecture
    Development Tight coupling Loose coupling
    Fault Isolation Single point of failure Isolated failures

    Project Structure

    A well-organized microservices project follows this structure:

    microservices-app/
    ├── services/
    │   ├── auth-service/
    │   │   ├── src/
    │   │   ├── Dockerfile
    │   │   └── package.json
    │   ├── user-service/
    │   │   ├── src/
    │   │   ├── Dockerfile
    │   │   └── package.json
    │   └── product-service/
    │       ├── src/
    │       ├── Dockerfile
    │       └── package.json
    ├── api-gateway/
    │   ├── src/
    │   ├── Dockerfile
    │   └── package.json
    ├── docker-compose.yml
    └── README.md
    

    Building the Authentication Service

    Let’s start with a critical service – authentication. This service handles user registration, login, and JWT token generation.

    Setting Up the Project

    mkdir auth-service && cd auth-service
    npm init -y
    npm install express jsonwebtoken bcryptjs dotenv cors helmet
    npm install --save-dev nodemon
    

    Authentication Service Code

    // auth-service/src/index.js
    const express = require('express');
    const jwt = require('jsonwebtoken');
    const bcrypt = require('bcryptjs');
    const helmet = require('helmet');
    const cors = require('cors');
    
    const app = express();
    const PORT = process.env.PORT || 3001;
    const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
    
    // Middleware
    app.use(helmet());
    app.use(cors());
    app.use(express.json());
    
    // In-memory user store (use database in production)
    const users = new Map();
    
    // Health check endpoint
    app.get('/health', (req, res) => {
      res.json({ status: 'healthy', service: 'auth-service' });
    });
    
    // Register endpoint
    app.post('/api/auth/register', async (req, res) => {
      try {
        const { username, email, password } = req.body;
        
        // Validation
        if (!username || !email || !password) {
          return res.status(400).json({ 
            error: 'Missing required fields' 
          });
        }
        
        // Check if user exists
        if (users.has(email)) {
          return res.status(409).json({ 
            error: 'User already exists' 
          });
        }
        
        // Hash password
        const hashedPassword = await bcrypt.hash(password, 10);
        
        // Store user
        const user = {
          id: Date.now().toString(),
          username,
          email,
          password: hashedPassword,
          createdAt: new Date()
        };
        
        users.set(email, user);
        
        // Generate token
        const token = jwt.sign(
          { id: user.id, email: user.email },
          JWT_SECRET,
          { expiresIn: '24h' }
        );
        
        res.status(201).json({
          message: 'User registered successfully',
          token,
          user: {
            id: user.id,
            username: user.username,
            email: user.email
          }
        });
        
      } catch (error) {
        console.error('Registration error:', error);
        res.status(500).json({ error: 'Internal server error' });
      }
    });
    
    // Login endpoint
    app.post('/api/auth/login', async (req, res) => {
      try {
        const { email, password } = req.body;
        
        // Validation
        if (!email || !password) {
          return res.status(400).json({ 
            error: 'Missing credentials' 
          });
        }
        
        // Find user
        const user = users.get(email);
        if (!user) {
          return res.status(401).json({ 
            error: 'Invalid credentials' 
          });
        }
        
        // Verify password
        const isValid = await bcrypt.compare(password, user.password);
        if (!isValid) {
          return res.status(401).json({ 
            error: 'Invalid credentials' 
          });
        }
        
        // Generate token
        const token = jwt.sign(
          { id: user.id, email: user.email },
          JWT_SECRET,
          { expiresIn: '24h' }
        );
        
        res.json({
          message: 'Login successful',
          token,
          user: {
            id: user.id,
            username: user.username,
            email: user.email
          }
        });
        
      } catch (error) {
        console.error('Login error:', error);
        res.status(500).json({ error: 'Internal server error' });
      }
    });
    
    // Verify token endpoint
    app.post('/api/auth/verify', (req, res) => {
      try {
        const token = req.headers.authorization?.split(' ')[1];
        
        if (!token) {
          return res.status(401).json({ error: 'No token provided' });
        }
        
        const decoded = jwt.verify(token, JWT_SECRET);
        res.json({ valid: true, user: decoded });
        
      } catch (error) {
        res.status(401).json({ valid: false, error: 'Invalid token' });
      }
    });
    
    app.listen(PORT, () => {
      console.log(`Auth service running on port ${PORT}`);
    });
    

    Dockerfile for Auth Service

    # auth-service/Dockerfile
    FROM node:18-alpine
    
    WORKDIR /app
    
    # Copy package files
    COPY package*.json ./
    
    # Install dependencies
    RUN npm ci --only=production
    
    # Copy source code
    COPY src ./src
    
    # Expose port
    EXPOSE 3001
    
    # Health check
    HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 
      CMD node -e "require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
    
    # Start service
    CMD ["node", "src/index.js"]
    

    Building the User Service

    The user service manages user profiles and preferences.

    // user-service/src/index.js
    const express = require('express');
    const helmet = require('helmet');
    const cors = require('cors');
    const axios = require('axios');
    
    const app = express();
    const PORT = process.env.PORT || 3002;
    const AUTH_SERVICE_URL = process.env.AUTH_SERVICE_URL || 'http://auth-service:3001';
    
    app.use(helmet());
    app.use(cors());
    app.use(express.json());
    
    // Middleware to verify JWT
    const authenticateToken = async (req, res, next) => {
      try {
        const token = req.headers.authorization?.split(' ')[1];
        
        if (!token) {
          return res.status(401).json({ error: 'Authentication required' });
        }
        
        // Verify token with auth service
        const response = await axios.post(
          `${AUTH_SERVICE_URL}/api/auth/verify`,
          {},
          { headers: { authorization: `Bearer ${token}` } }
        );
        
        if (response.data.valid) {
          req.user = response.data.user;
          next();
        } else {
          res.status(401).json({ error: 'Invalid token' });
        }
        
      } catch (error) {
        res.status(401).json({ error: 'Authentication failed' });
      }
    };
    
    // In-memory user profiles
    const profiles = new Map();
    
    // Get user profile
    app.get('/api/users/profile', authenticateToken, (req, res) => {
      const profile = profiles.get(req.user.id) || {
        id: req.user.id,
        email: req.user.email,
        bio: '',
        avatar: '',
        preferences: {}
      };
      
      res.json(profile);
    });
    
    // Update user profile
    app.put('/api/users/profile', authenticateToken, (req, res) => {
      const { bio, avatar, preferences } = req.body;
      
      const profile = {
        id: req.user.id,
        email: req.user.email,
        bio: bio || '',
        avatar: avatar || '',
        preferences: preferences || {},
        updatedAt: new Date()
      };
      
      profiles.set(req.user.id, profile);
      res.json(profile);
    });
    
    app.listen(PORT, () => {
      console.log(`User service running on port ${PORT}`);
    });
    

    API Gateway with Express

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

    // api-gateway/src/index.js
    const express = require('express');
    const { createProxyMiddleware } = require('http-proxy-middleware');
    const helmet = require('helmet');
    const cors = require('cors');
    const rateLimit = require('express-rate-limit');
    
    const app = express();
    const PORT = process.env.PORT || 3000;
    
    // Security middleware
    app.use(helmet());
    app.use(cors());
    app.use(express.json());
    
    // Rate limiting
    const limiter = rateLimit({
      windowMs: 15 * 60 * 1000, // 15 minutes
      max: 100 // limit each IP to 100 requests per windowMs
    });
    
    app.use('/api/', limiter);
    
    // Service URLs
    const services = {
      auth: process.env.AUTH_SERVICE_URL || 'http://auth-service:3001',
      user: process.env.USER_SERVICE_URL || 'http://user-service:3002',
      product: process.env.PRODUCT_SERVICE_URL || 'http://product-service:3003'
    };
    
    // Route to auth service
    app.use('/api/auth', createProxyMiddleware({
      target: services.auth,
      changeOrigin: true,
      pathRewrite: {
        '^/api/auth': '/api/auth'
      }
    }));
    
    // Route to user service
    app.use('/api/users', createProxyMiddleware({
      target: services.user,
      changeOrigin: true,
      pathRewrite: {
        '^/api/users': '/api/users'
      }
    }));
    
    // Route to product service
    app.use('/api/products', createProxyMiddleware({
      target: services.product,
      changeOrigin: true,
      pathRewrite: {
        '^/api/products': '/api/products'
      }
    }));
    
    // Health check
    app.get('/health', (req, res) => {
      res.json({ status: 'healthy', service: 'api-gateway' });
    });
    
    app.listen(PORT, () => {
      console.log(`API Gateway running on port ${PORT}`);
    });
    

    Docker Compose Configuration

    Docker Compose orchestrates all microservices together:

    # docker-compose.yml
    version: '3.8'
    
    services:
      # API Gateway
      api-gateway:
        build: ./api-gateway
        ports:
          - "3000:3000"
        environment:
          - AUTH_SERVICE_URL=http://auth-service:3001
          - USER_SERVICE_URL=http://user-service:3002
          - PRODUCT_SERVICE_URL=http://product-service:3003
        depends_on:
          - auth-service
          - user-service
          - product-service
        networks:
          - microservices-network
    
      # Auth Service
      auth-service:
        build: ./services/auth-service
        ports:
          - "3001:3001"
        environment:
          - PORT=3001
          - JWT_SECRET=your-super-secret-jwt-key
        networks:
          - microservices-network
        healthcheck:
          test: ["CMD", "curl", "-f", "http://localhost:3001/health"]
          interval: 30s
          timeout: 10s
          retries: 3
    
      # User Service
      user-service:
        build: ./services/user-service
        ports:
          - "3002:3002"
        environment:
          - PORT=3002
          - AUTH_SERVICE_URL=http://auth-service:3001
        depends_on:
          - auth-service
        networks:
          - microservices-network
    
      # Product Service
      product-service:
        build: ./services/product-service
        ports:
          - "3003:3003"
        environment:
          - PORT=3003
          - AUTH_SERVICE_URL=http://auth-service:3001
        depends_on:
          - auth-service
        networks:
          - microservices-network
    
      # Redis for caching
      redis:
        image: redis:7-alpine
        ports:
          - "6379:6379"
        networks:
          - microservices-network
    
      # PostgreSQL database
      postgres:
        image: postgres:15-alpine
        environment:
          - POSTGRES_USER=admin
          - POSTGRES_PASSWORD=password
          - POSTGRES_DB=microservices
        ports:
          - "5432:5432"
        volumes:
          - postgres-data:/var/lib/postgresql/data
        networks:
          - microservices-network
    
    networks:
      microservices-network:
        driver: bridge
    
    volumes:
      postgres-data:
    

    Running the Microservices

    Deploy your microservices ecosystem with these commands:

    # Build all services
    docker-compose build
    
    # Start all services
    docker-compose up -d
    
    # View logs
    docker-compose logs -f
    
    # Check service health
    docker-compose ps
    
    # Stop all services
    docker-compose down
    

    Testing the Microservices

    Test your microservices with these API calls:

    # Register a new user
    curl -X POST http://localhost:3000/api/auth/register 
      -H "Content-Type: application/json" 
      -d '{
        "username": "john_doe",
        "email": "john@example.com",
        "password": "SecurePass123!"
      }'
    
    # Login
    curl -X POST http://localhost:3000/api/auth/login 
      -H "Content-Type: application/json" 
      -d '{
        "email": "john@example.com",
        "password": "SecurePass123!"
      }'
    
    # Get user profile (use token from login)
    curl -X GET http://localhost:3000/api/users/profile 
      -H "Authorization: Bearer YOUR_JWT_TOKEN"
    

    Monitoring and Logging

    Implement centralized logging with Winston and monitoring with Prometheus:

    Tool Purpose Integration
    Winston Structured logging npm install winston
    Prometheus Metrics collection prom-client library
    Grafana Visualization Docker container
    Jaeger Distributed tracing OpenTelemetry

    Best Practices

    Follow these best practices for production-ready microservices:

    • Service Discovery: Use Consul or Eureka for dynamic service registration
    • Circuit Breakers: Implement with libraries like Opossum to prevent cascade failures
    • API Versioning: Version your APIs (e.g., /api/v1/users) for backward compatibility
    • Database per Service: Each microservice should have its own database
    • Event-Driven Communication: Use message queues (RabbitMQ, Kafka) for async communication
    • Security: Implement OAuth2, rate limiting, and input validation
    • Documentation: Use Swagger/OpenAPI for API documentation
    • Testing: Write unit, integration, and end-to-end tests

    Scaling Strategies

    Scale your microservices effectively:

    # Scale specific service to 3 instances
    docker-compose up -d --scale user-service=3
    
    # With Kubernetes
    kubectl scale deployment user-service --replicas=5
    

    Conclusion

    Building microservices with Node.js and Docker provides a robust, scalable architecture for modern applications. This guide covered the fundamentals, from service creation to deployment and monitoring. Start small, iterate, and gradually expand your microservices ecosystem as your application grows.

    Remember: microservices add complexity, so only adopt them when your application truly needs the scalability and flexibility they provide.

    Leave a Reply

    Your email address will not be published. Required fields are marked *