Arrow

Complete Guide to Building Microservices with Node.js and Docker

Share this article:

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.

Node.js Docker Microservices DevOps Architecture

Responses

No responses yet

Table of Contents

Arrow

JOIN OUR NEWSLETTER

Subscribe our newsletter to receive the latest news and exclusive offers every week. No spam.

We use cookies to improve your experience. By using our site, you agree to our Cookie Policy.