1.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.
2.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 |
3.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
4.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"]
5.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}`);
});
6.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}`);
});
7.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:
8.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
9.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"
10.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 |
11.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
12.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
13.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.
