Claude-skill-registry express-microservices-architecture
Complete guide for building scalable microservices with Express.js including middleware patterns, routing strategies, error handling, production architecture, and deployment best practices
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/express-microservices-architecture" ~/.claude/skills/majiayu000-claude-skill-registry-express-microservices-architecture && rm -rf "$T"
skills/data/express-microservices-architecture/SKILL.mdExpress.js Microservices Architecture
A comprehensive skill for building production-ready microservices with Express.js. Master middleware patterns, routing strategies, error handling, scalability techniques, and deployment architectures for Node.js microservices at scale.
When to Use This Skill
Use this skill when:
- Building RESTful APIs and microservices with Node.js
- Designing scalable distributed systems with Express.js
- Implementing middleware-based architecture patterns
- Creating API gateways and service mesh architectures
- Developing production-ready Node.js applications
- Migrating monoliths to microservices architecture
- Building event-driven microservices
- Implementing authentication, authorization, and security layers
- Optimizing Express.js applications for high performance
- Setting up monitoring, logging, and observability
- Deploying Express.js apps with Docker and Kubernetes
- Implementing circuit breakers and resilience patterns
Core Concepts
Express.js Fundamentals
Express.js is a minimal and flexible Node.js web application framework that provides robust features for web and mobile applications. It's the de facto standard for building Node.js APIs and microservices.
Key Characteristics:
- Minimal: Unopinionated framework with essential web app features
- Middleware-based: Request/response pipeline architecture
- Routing: Powerful routing mechanism with parameter support
- Template Engines: Support for various view engines
- Performance: Built on top of Node.js for high performance
- Extensible: Rich ecosystem of middleware and plugins
Middleware Architecture
Middleware functions are the backbone of Express.js applications. They have access to the request object (
req), response object (res), and the next middleware function (next).
Middleware Flow:
Request → Middleware 1 → Middleware 2 → ... → Route Handler → Response ↓ ↓ ↓ Error Handler Error Handler Error Handler
Middleware Types:
- Application-level middleware: Bound to
instanceapp - Router-level middleware: Bound to
instanceexpress.Router() - Error-handling middleware: Has 4 parameters (err, req, res, next)
- Built-in middleware: Express built-in functions (static, json, urlencoded)
- Third-party middleware: External packages (cors, helmet, morgan)
Routing Strategies
Express routing enables you to map HTTP methods and URLs to handler functions.
Routing Components:
- Route paths: String patterns, regex, or path parameters
- Route parameters: Named URL segments (:userId)
- Route handlers: Single or multiple callback functions
- Response methods: res.send(), res.json(), res.status(), etc.
- Router instances: Modular, mountable route handlers
Error Handling
Error handling in Express requires special middleware with 4 parameters:
(err, req, res, next).
Error Handling Flow:
- Synchronous errors are caught automatically
- Asynchronous errors must be passed to
next(err) - Error middleware processes errors centrally
- Proper status codes and error formats returned
Microservices Principles
Characteristics of Microservices:
- Single Responsibility: Each service does one thing well
- Independence: Services can be deployed independently
- Decentralized: Each service owns its data
- Resilience: Failure in one service doesn't crash entire system
- Scalability: Scale services independently based on demand
- Technology Diversity: Different services can use different tech stacks
Microservices Patterns
Pattern 1: API Gateway Pattern
The API Gateway acts as a single entry point for all client requests, routing them to appropriate microservices.
Benefits:
- Single entry point for clients
- Request routing and composition
- Authentication and authorization
- Rate limiting and throttling
- Request/response transformation
- Protocol translation
Implementation Structure:
Client → API Gateway → Microservice 1 (Users) → Microservice 2 (Orders) → Microservice 3 (Products) → Microservice 4 (Notifications)
Pattern 2: Service Discovery
Services register themselves and discover other services dynamically.
Approaches:
- Client-side discovery: Client queries service registry
- Server-side discovery: Load balancer queries registry
- DNS-based discovery: Using DNS for service location
Popular Tools:
- Consul
- Eureka
- etcd
- Kubernetes built-in discovery
Pattern 3: Circuit Breaker
Prevents cascading failures by stopping requests to failing services.
States:
- Closed: Normal operation, requests pass through
- Open: Service failing, requests fail immediately
- Half-Open: Testing if service recovered
Pattern 4: Event-Driven Architecture
Services communicate through events instead of direct calls.
Components:
- Event producers: Services that emit events
- Event consumers: Services that listen to events
- Message broker: RabbitMQ, Kafka, Redis
- Event store: Persist events for replay
Pattern 5: Database per Service
Each microservice owns its database, ensuring loose coupling.
Benefits:
- Service independence
- Technology diversity
- Easier scaling
- Clear boundaries
Challenges:
- Distributed transactions
- Data consistency
- Joins across services
Pattern 6: Saga Pattern
Manages distributed transactions across multiple services.
Types:
- Choreography: Services coordinate through events
- Orchestration: Central coordinator manages transaction
Pattern 7: CQRS (Command Query Responsibility Segregation)
Separate read and write operations into different models.
Benefits:
- Optimized read/write models
- Scalability
- Performance
- Flexibility
Middleware Architecture Patterns
Custom Middleware Development
Middleware functions execute in the order they're defined.
Basic Middleware Structure:
const express = require('express'); const app = express(); // Basic middleware const requestLogger = (req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); // Pass control to next middleware }; app.use(requestLogger);
From Context7 - Saving Data in Request Object:
const express = require('express'); const app = express(); const port = 3000; // Middleware to add user data to the request object const addUserInfo = (req, res, next) => { req.user = { id: 123, username: 'testuser' }; next(); }; // Middleware to add request timestamp const addTimestamp = (req, res, next) => { req.requestTime = Date.now(); next(); }; // Apply middleware globally app.use(addUserInfo); app.use(addTimestamp); app.get('/', (req, res) => { const userId = req.user.id; const username = req.user.username; const timestamp = req.requestTime; res.send(`User ID: ${userId}, Username: ${username}, Request Time: ${new Date(timestamp).toISOString()}`); }); app.listen(port, () => { console.log(`Request data sharing example listening at http://localhost:${port}`); });
Error-Handling Middleware
Error middleware has 4 parameters and should be defined after all other middleware.
From Context7 - Error Handling Middleware:
const express = require('express'); const app = express(); const port = 3000; // A regular middleware app.use((req, res, next) => { console.log('Request received'); next(); // Pass control to the next middleware }); // A route that might throw an error app.get('/throw-error', (req, res, next) => { // Simulate an error const error = new Error('This is a simulated error'); error.status = 400; next(error); }); // Error-handling middleware (must have 4 arguments) app.use((err, req, res, next) => { console.error('Error caught:', err.message); res.status(err.status || 500).send(`An error occurred: ${err.message}`); }); app.listen(port, () => { console.log(`Error middleware example listening at http://localhost:${port}`); });
From Context7 - Global Error Handler:
app.use(express.bodyParser()) app.use(express.cookieParser()) app.use(express.session()) app.use(app.router) // the router itself (app.get(), app.put() etc) app.use(function(err, req, res, next){ // if an error occurs Connect will pass it down // through these "error-handling" middleware // allowing you to respond however you like res.send(500, { error: 'Sorry something bad happened!' }); })
Route-Specific Middleware
Apply middleware to specific routes for targeted functionality.
From Context7 - Route Middleware:
const express = require('express'); const app = express(); const port = 3000; // Middleware function const requestLogger = (req, res, next) => { console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`); next(); }; // Apply middleware to a specific route app.get('/protected', requestLogger, (req, res) => { res.send('This route is protected by middleware!'); }); // Apply middleware to multiple routes const adminMiddleware = (req, res, next) => { console.log('Admin access check...'); // In a real app, you'd check user roles here next(); }; app.get('/admin/dashboard', adminMiddleware, (req, res) => { res.send('Welcome to the admin dashboard!'); }); // Middleware applied globally app.use(requestLogger); app.get('/', (req, res) => { res.send('Hello, world!'); }); app.listen(port, () => { console.log(`Route middleware example listening at http://localhost:${port}`); });
Authentication Middleware
const jwt = require('jsonwebtoken'); // JWT Authentication Middleware const authenticateToken = (req, res, next) => { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'Access token required' }); } jwt.verify(token, process.env.JWT_SECRET, (err, user) => { if (err) { return res.status(403).json({ error: 'Invalid or expired token' }); } req.user = user; next(); }); }; // Role-based Authorization Middleware const authorize = (...roles) => { return (req, res, next) => { if (!req.user) { return res.status(401).json({ error: 'Unauthorized' }); } if (!roles.includes(req.user.role)) { return res.status(403).json({ error: 'Insufficient permissions' }); } next(); }; }; // Usage app.get('/admin/users', authenticateToken, authorize('admin'), (req, res) => { res.json({ users: [] }); });
Request Validation Middleware
const { body, param, query, validationResult } = require('express-validator'); // Validation middleware factory const validate = (validations) => { return async (req, res, next) => { await Promise.all(validations.map(validation => validation.run(req))); const errors = validationResult(req); if (errors.isEmpty()) { return next(); } res.status(400).json({ error: 'Validation failed', details: errors.array() }); }; }; // Usage app.post('/users', validate([ body('email').isEmail().normalizeEmail(), body('password').isLength({ min: 8 }), body('name').trim().notEmpty() ]), (req, res) => { // Request is validated res.json({ success: true }); });
Rate Limiting Middleware
const rateLimit = require('express-rate-limit'); const RedisStore = require('rate-limit-redis'); const Redis = require('ioredis'); // In-memory rate limiter const limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests, please try again later', standardHeaders: true, legacyHeaders: false, }); // Redis-based rate limiter for distributed systems const redisClient = new Redis({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT, }); const distributedLimiter = rateLimit({ store: new RedisStore({ client: redisClient, prefix: 'rl:', }), windowMs: 15 * 60 * 1000, max: 100, }); // Apply to all routes app.use('/api/', distributedLimiter); // Apply to specific routes app.post('/api/login', rateLimit({ windowMs: 15 * 60 * 1000, max: 5, // Only 5 login attempts per 15 minutes }), loginHandler);
Logging Middleware
const morgan = require('morgan'); const winston = require('winston'); const { format } = winston; // Create Winston logger const logger = winston.createLogger({ level: 'info', format: format.combine( format.timestamp(), format.errors({ stack: true }), format.json() ), defaultMeta: { service: 'user-service' }, transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'combined.log' }), ], }); // Console logging in development if (process.env.NODE_ENV !== 'production') { logger.add(new winston.transports.Console({ format: format.combine( format.colorize(), format.simple() ) })); } // HTTP request logging with Morgan app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } })); // Custom logging middleware const requestLogger = (req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = Date.now() - start; logger.info({ method: req.method, path: req.path, status: res.statusCode, duration: `${duration}ms`, ip: req.ip, userAgent: req.get('user-agent') }); }); next(); }; app.use(requestLogger);
CORS Middleware
const cors = require('cors'); // Basic CORS app.use(cors()); // Configured CORS const corsOptions = { origin: function (origin, callback) { const allowedOrigins = [ 'https://example.com', 'https://app.example.com', process.env.FRONTEND_URL ]; if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, optionsSuccessStatus: 200, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], allowedHeaders: ['Content-Type', 'Authorization'], exposedHeaders: ['X-Total-Count', 'X-Page-Number'], maxAge: 86400, // 24 hours }; app.use(cors(corsOptions)); // CORS for specific routes app.options('/api/admin/*', cors(adminCorsOptions)); app.use('/api/admin/', cors(adminCorsOptions));
Security Middleware
const helmet = require('helmet'); const mongoSanitize = require('express-mongo-sanitize'); const xss = require('xss-clean'); const hpp = require('hpp'); // Helmet - Set security headers app.use(helmet()); // Custom security headers app.use((req, res, next) => { res.setHeader('X-Content-Type-Options', 'nosniff'); res.setHeader('X-Frame-Options', 'DENY'); res.setHeader('X-XSS-Protection', '1; mode=block'); next(); }); // Sanitize data against NoSQL injection app.use(mongoSanitize()); // Prevent XSS attacks app.use(xss()); // Prevent HTTP Parameter Pollution app.use(hpp({ whitelist: ['sort', 'fields', 'page', 'limit'] })); // Content Security Policy app.use(helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", 'data:', 'https:'], }, }));
Routing Strategies
Basic Routing
From Context7 - Express Routing:
app.get('/', home); app.use('/public', require('st')(process.cwd())); app.get('/users', users.list); app.post('/users', users.create);
Route Method Chaining
From Context7 - Route Chaining:
app.route('/users') .get(function(req, res, next) { // Get all users res.json({ users: [] }); }) .post(function(req, res, next) { // Create new user res.status(201).json({ user: {} }); });
Router Modules
// routes/users.js const express = require('express'); const router = express.Router(); // Middleware specific to this router router.use((req, res, next) => { console.log('Time: ', Date.now()); next(); }); // Define routes router.get('/', (req, res) => { res.json({ users: [] }); }); router.get('/:id', (req, res) => { res.json({ user: { id: req.params.id } }); }); router.post('/', (req, res) => { res.status(201).json({ user: req.body }); }); router.put('/:id', (req, res) => { res.json({ user: { id: req.params.id, ...req.body } }); }); router.delete('/:id', (req, res) => { res.status(204).send(); }); module.exports = router; // app.js const usersRouter = require('./routes/users'); app.use('/api/users', usersRouter);
Route Parameters
// Named parameters app.get('/users/:userId/posts/:postId', (req, res) => { const { userId, postId } = req.params; res.json({ userId, postId }); }); // Parameter middleware app.param('userId', (req, res, next, userId) => { // Fetch user from database User.findById(userId) .then(user => { if (!user) { return res.status(404).json({ error: 'User not found' }); } req.user = user; next(); }) .catch(next); }); // Multiple callbacks app.param('postId', [ validatePostId, fetchPost, checkPermissions ]);
Query Parameters
// GET /api/users?role=admin&active=true&page=2&limit=10 app.get('/api/users', (req, res) => { const { role, active, page = 1, limit = 10, sort = '-createdAt' } = req.query; const query = {}; if (role) query.role = role; if (active !== undefined) query.active = active === 'true'; const skip = (page - 1) * limit; User.find(query) .sort(sort) .limit(parseInt(limit)) .skip(skip) .then(users => res.json({ users, page, limit })) .catch(next); });
API Versioning
// Version 1 routes const v1Router = express.Router(); v1Router.get('/users', (req, res) => { res.json({ version: 'v1', users: [] }); }); // Version 2 routes const v2Router = express.Router(); v2Router.get('/users', (req, res) => { res.json({ version: 'v2', users: [], meta: {} }); }); // Mount versioned routes app.use('/api/v1', v1Router); app.use('/api/v2', v2Router); // Header-based versioning app.use('/api/users', (req, res, next) => { const version = req.headers['api-version'] || 'v1'; if (version === 'v2') { return v2UsersHandler(req, res, next); } return v1UsersHandler(req, res, next); });
RESTful Route Organization
// controllers/users.controller.js class UsersController { async list(req, res, next) { try { const users = await User.find(); res.json({ users }); } catch (error) { next(error); } } async get(req, res, next) { try { res.json({ user: req.user }); } catch (error) { next(error); } } async create(req, res, next) { try { const user = await User.create(req.body); res.status(201).json({ user }); } catch (error) { next(error); } } async update(req, res, next) { try { const user = await User.findByIdAndUpdate( req.params.id, req.body, { new: true, runValidators: true } ); res.json({ user }); } catch (error) { next(error); } } async delete(req, res, next) { try { await User.findByIdAndDelete(req.params.id); res.status(204).send(); } catch (error) { next(error); } } } // routes/users.routes.js const router = express.Router(); const controller = new UsersController(); router.get('/', controller.list); router.get('/:id', controller.get); router.post('/', controller.create); router.put('/:id', controller.update); router.delete('/:id', controller.delete); module.exports = router;
Scalability Patterns
Horizontal Scaling
Deploy multiple instances of your service behind a load balancer.
// Enable cluster mode const cluster = require('cluster'); const os = require('os'); if (cluster.isMaster) { const numCPUs = os.cpus().length; console.log(`Master process ${process.pid} is running`); console.log(`Forking ${numCPUs} workers...`); // Fork workers for (let i = 0; i < numCPUs; i++) { cluster.fork(); } cluster.on('exit', (worker, code, signal) => { console.log(`Worker ${worker.process.pid} died. Restarting...`); cluster.fork(); }); } else { // Workers share the TCP connection const app = require('./app'); const port = process.env.PORT || 3000; app.listen(port, () => { console.log(`Worker ${process.pid} started on port ${port}`); }); }
Load Balancing
# nginx.conf upstream backend { least_conn; server localhost:3001; server localhost:3002; server localhost:3003; server localhost:3004; } server { listen 80; location / { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
Caching Strategies
const Redis = require('ioredis'); const redis = new Redis({ host: process.env.REDIS_HOST, port: process.env.REDIS_PORT, }); // Cache middleware const cacheMiddleware = (duration = 300) => { return async (req, res, next) => { if (req.method !== 'GET') { return next(); } const key = `cache:${req.originalUrl}`; try { const cached = await redis.get(key); if (cached) { return res.json(JSON.parse(cached)); } // Store original res.json const originalJson = res.json.bind(res); // Override res.json res.json = (body) => { redis.setex(key, duration, JSON.stringify(body)); return originalJson(body); }; next(); } catch (error) { console.error('Cache error:', error); next(); } }; }; // Usage app.get('/api/products', cacheMiddleware(600), async (req, res) => { const products = await Product.find(); res.json({ products }); }); // Cache invalidation const invalidateCache = async (pattern) => { const keys = await redis.keys(pattern); if (keys.length > 0) { await redis.del(...keys); } }; // Invalidate on updates app.post('/api/products', async (req, res) => { const product = await Product.create(req.body); await invalidateCache('cache:/api/products*'); res.status(201).json({ product }); });
Database Connection Pooling
const mongoose = require('mongoose'); // MongoDB connection with pooling mongoose.connect(process.env.MONGODB_URI, { useNewUrlParser: true, useUnifiedTopology: true, poolSize: 10, // Maintain up to 10 socket connections socketTimeoutMS: 45000, family: 4, }); // PostgreSQL with connection pooling const { Pool } = require('pg'); const pool = new Pool({ host: process.env.DB_HOST, port: process.env.DB_PORT, database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 20, // Maximum number of clients idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); // Query helper const query = async (text, params) => { const start = Date.now(); const res = await pool.query(text, params); const duration = Date.now() - start; console.log('Executed query', { text, duration, rows: res.rowCount }); return res; }; module.exports = { query, pool };
Response Compression
const compression = require('compression'); // Basic compression app.use(compression()); // Custom compression settings app.use(compression({ filter: (req, res) => { if (req.headers['x-no-compression']) { return false; } return compression.filter(req, res); }, level: 6, // Compression level (0-9) threshold: 1024, // Minimum size to compress (bytes) }));
Request Throttling
const { Throttle } = require('stream-throttle'); // Throttle large responses app.get('/api/large-dataset', (req, res) => { const dataStream = getLargeDataStream(); // Throttle to 1MB/s const throttle = new Throttle({ rate: 1024 * 1024 }); res.setHeader('Content-Type', 'application/json'); dataStream.pipe(throttle).pipe(res); });
Production Architecture
Docker Containerization
# Dockerfile FROM node:18-alpine AS builder WORKDIR /app # Copy package files COPY package*.json ./ # Install dependencies RUN npm ci --only=production # Copy source code COPY . . # Production image FROM node:18-alpine WORKDIR /app # Create non-root user RUN addgroup -g 1001 -S nodejs && \ adduser -S nodejs -u 1001 # Copy from builder COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules COPY --chown=nodejs:nodejs . . # Switch to non-root user USER nodejs # Expose port EXPOSE 3000 # Health check HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \ CMD node healthcheck.js # Start application CMD ["node", "server.js"]
# docker-compose.yml version: '3.8' services: api: build: . ports: - "3000:3000" environment: - NODE_ENV=production - REDIS_HOST=redis - MONGODB_URI=mongodb://mongo:27017/app depends_on: - redis - mongo restart: unless-stopped deploy: replicas: 3 resources: limits: cpus: '0.5' memory: 512M redis: image: redis:7-alpine volumes: - redis-data:/data restart: unless-stopped mongo: image: mongo:6 volumes: - mongo-data:/data/db restart: unless-stopped nginx: image: nginx:alpine ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf depends_on: - api restart: unless-stopped volumes: redis-data: mongo-data:
Process Management with PM2
// ecosystem.config.js module.exports = { apps: [{ name: 'api', script: './server.js', instances: 'max', exec_mode: 'cluster', autorestart: true, watch: false, max_memory_restart: '1G', env: { NODE_ENV: 'development', PORT: 3000 }, env_production: { NODE_ENV: 'production', PORT: 3000 }, error_file: './logs/err.log', out_file: './logs/out.log', log_file: './logs/combined.log', time: true, merge_logs: true, }] };
Health Checks
// healthcheck.js const http = require('http'); const options = { host: 'localhost', port: process.env.PORT || 3000, path: '/health', timeout: 2000 }; const healthCheck = http.request(options, (res) => { console.log(`HEALTHCHECK STATUS: ${res.statusCode}`); if (res.statusCode === 200) { process.exit(0); } else { process.exit(1); } }); healthCheck.on('error', (err) => { console.error('ERROR:', err); process.exit(1); }); healthCheck.end(); // Health endpoint app.get('/health', async (req, res) => { const health = { uptime: process.uptime(), message: 'OK', timestamp: Date.now() }; try { // Check database connection await mongoose.connection.db.admin().ping(); health.database = 'connected'; // Check Redis connection await redis.ping(); health.cache = 'connected'; res.status(200).json(health); } catch (error) { health.message = error.message; res.status(503).json(health); } }); // Readiness check app.get('/ready', (req, res) => { res.status(200).json({ ready: true }); }); // Liveness check app.get('/live', (req, res) => { res.status(200).json({ alive: true }); });
Graceful Shutdown
// server.js const gracefulShutdown = () => { console.log('Received shutdown signal, closing server gracefully...'); server.close(async () => { console.log('HTTP server closed'); try { // Close database connections await mongoose.connection.close(); console.log('MongoDB connection closed'); // Close Redis connection await redis.quit(); console.log('Redis connection closed'); // Close other resources // ... console.log('Graceful shutdown completed'); process.exit(0); } catch (err) { console.error('Error during shutdown:', err); process.exit(1); } }); // Force shutdown after 10 seconds setTimeout(() => { console.error('Forcing shutdown after timeout'); process.exit(1); }, 10000); }; process.on('SIGTERM', gracefulShutdown); process.on('SIGINT', gracefulShutdown);
Monitoring and Observability
const promClient = require('prom-client'); // Create a Registry const register = new promClient.Registry(); // Add default metrics promClient.collectDefaultMetrics({ register }); // Custom metrics const httpRequestDuration = new promClient.Histogram({ name: 'http_request_duration_seconds', help: 'Duration of HTTP requests in seconds', labelNames: ['method', 'route', 'status_code'], buckets: [0.1, 0.5, 1, 2, 5] }); const httpRequestTotal = new promClient.Counter({ name: 'http_requests_total', help: 'Total number of HTTP requests', labelNames: ['method', 'route', 'status_code'] }); register.registerMetric(httpRequestDuration); register.registerMetric(httpRequestTotal); // Metrics middleware app.use((req, res, next) => { const start = Date.now(); res.on('finish', () => { const duration = (Date.now() - start) / 1000; const route = req.route ? req.route.path : req.path; httpRequestDuration.labels(req.method, route, res.statusCode).observe(duration); httpRequestTotal.labels(req.method, route, res.statusCode).inc(); }); next(); }); // Metrics endpoint app.get('/metrics', async (req, res) => { res.set('Content-Type', register.contentType); res.end(await register.metrics()); });
Distributed Tracing
const { trace, context } = require('@opentelemetry/api'); const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); const { registerInstrumentations } = require('@opentelemetry/instrumentation'); const { HttpInstrumentation } = require('@opentelemetry/instrumentation-http'); const { ExpressInstrumentation } = require('@opentelemetry/instrumentation-express'); // Create tracer provider const provider = new NodeTracerProvider(); provider.register(); // Register instrumentations registerInstrumentations({ instrumentations: [ new HttpInstrumentation(), new ExpressInstrumentation(), ], }); const tracer = trace.getTracer('user-service'); // Custom tracing middleware const tracingMiddleware = (req, res, next) => { const span = tracer.startSpan(`HTTP ${req.method} ${req.path}`); span.setAttributes({ 'http.method': req.method, 'http.url': req.url, 'http.user_agent': req.get('user-agent'), }); res.on('finish', () => { span.setAttributes({ 'http.status_code': res.statusCode, }); span.end(); }); next(); }; app.use(tracingMiddleware);
Best Practices
Project Structure
express-microservice/ ├── src/ │ ├── config/ │ │ ├── database.js │ │ ├── redis.js │ │ └── logger.js │ ├── controllers/ │ │ ├── users.controller.js │ │ └── auth.controller.js │ ├── middleware/ │ │ ├── auth.js │ │ ├── validation.js │ │ ├── errorHandler.js │ │ └── requestLogger.js │ ├── models/ │ │ └── user.model.js │ ├── routes/ │ │ ├── index.js │ │ ├── users.routes.js │ │ └── auth.routes.js │ ├── services/ │ │ ├── users.service.js │ │ ├── auth.service.js │ │ └── email.service.js │ ├── utils/ │ │ ├── apiError.js │ │ ├── catchAsync.js │ │ └── validators.js │ ├── app.js │ └── server.js ├── tests/ │ ├── unit/ │ ├── integration/ │ └── e2e/ ├── .env ├── .env.example ├── .dockerignore ├── Dockerfile ├── docker-compose.yml ├── ecosystem.config.js ├── package.json └── README.md
Environment Configuration
// config/env.js const dotenv = require('dotenv'); const Joi = require('joi'); dotenv.config(); const envSchema = Joi.object({ NODE_ENV: Joi.string() .valid('development', 'production', 'test') .default('development'), PORT: Joi.number().default(3000), MONGODB_URI: Joi.string().required(), REDIS_HOST: Joi.string().required(), REDIS_PORT: Joi.number().default(6379), JWT_SECRET: Joi.string().required(), JWT_EXPIRES_IN: Joi.string().default('7d'), LOG_LEVEL: Joi.string() .valid('error', 'warn', 'info', 'debug') .default('info'), }).unknown(); const { value: env, error } = envSchema.validate(process.env); if (error) { throw new Error(`Config validation error: ${error.message}`); } module.exports = { env: env.NODE_ENV, port: env.PORT, mongodb: { uri: env.MONGODB_URI, }, redis: { host: env.REDIS_HOST, port: env.REDIS_PORT, }, jwt: { secret: env.JWT_SECRET, expiresIn: env.JWT_EXPIRES_IN, }, logging: { level: env.LOG_LEVEL, }, };
Error Handling Best Practices
// utils/apiError.js class ApiError extends Error { constructor(statusCode, message, isOperational = true, stack = '') { super(message); this.statusCode = statusCode; this.isOperational = isOperational; if (stack) { this.stack = stack; } else { Error.captureStackTrace(this, this.constructor); } } } module.exports = ApiError; // utils/catchAsync.js const catchAsync = (fn) => { return (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); }; }; module.exports = catchAsync; // middleware/errorHandler.js const config = require('../config/env'); const logger = require('../config/logger'); const ApiError = require('../utils/apiError'); const errorConverter = (err, req, res, next) => { let error = err; if (!(error instanceof ApiError)) { const statusCode = error.statusCode || 500; const message = error.message || 'Internal Server Error'; error = new ApiError(statusCode, message, false, err.stack); } next(error); }; const errorHandler = (err, req, res, next) => { let { statusCode, message } = err; if (config.env === 'production' && !err.isOperational) { statusCode = 500; message = 'Internal Server Error'; } res.locals.errorMessage = err.message; const response = { code: statusCode, message, ...(config.env === 'development' && { stack: err.stack }), }; if (config.env === 'development') { logger.error(err); } res.status(statusCode).json(response); }; module.exports = { errorConverter, errorHandler, };
Testing Strategies
// tests/integration/users.test.js const request = require('supertest'); const app = require('../../src/app'); const { User } = require('../../src/models'); describe('User API', () => { beforeEach(async () => { await User.deleteMany({}); }); describe('POST /api/users', () => { it('should create a new user', async () => { const userData = { name: 'John Doe', email: 'john@example.com', password: 'password123', }; const res = await request(app) .post('/api/users') .send(userData) .expect(201); expect(res.body).toHaveProperty('user'); expect(res.body.user).toHaveProperty('id'); expect(res.body.user.email).toBe(userData.email); expect(res.body.user).not.toHaveProperty('password'); }); it('should return 400 for invalid email', async () => { const userData = { name: 'John Doe', email: 'invalid-email', password: 'password123', }; const res = await request(app) .post('/api/users') .send(userData) .expect(400); expect(res.body).toHaveProperty('error'); }); }); describe('GET /api/users/:id', () => { it('should return user by id', async () => { const user = await User.create({ name: 'John Doe', email: 'john@example.com', password: 'hashedpassword', }); const res = await request(app) .get(`/api/users/${user.id}`) .expect(200); expect(res.body.user.id).toBe(user.id); }); it('should return 404 for non-existent user', async () => { await request(app) .get('/api/users/507f1f77bcf86cd799439011') .expect(404); }); }); });
Performance Optimization
// Enable gzip compression const compression = require('compression'); app.use(compression()); // Use efficient JSON parsing app.use(express.json({ limit: '10mb' })); // Database query optimization const getUsers = async (filters) => { return User.find(filters) .select('name email role') // Select only needed fields .lean() // Return plain objects instead of Mongoose documents .limit(100); }; // Implement pagination const paginateResults = async (model, page = 1, limit = 10) => { const skip = (page - 1) * limit; const [results, total] = await Promise.all([ model.find().skip(skip).limit(limit).lean(), model.countDocuments(), ]); return { results, pagination: { page, limit, total, pages: Math.ceil(total / limit), }, }; }; // Use indexes userSchema.index({ email: 1 }); userSchema.index({ role: 1, createdAt: -1 }); // Connection pooling and keep-alive const agent = new http.Agent({ keepAlive: true, maxSockets: 50, });
Security Best Practices
// Validate and sanitize inputs const { body } = require('express-validator'); const userValidation = [ body('email').isEmail().normalizeEmail(), body('password').isLength({ min: 8 }).trim(), body('name').trim().escape(), ]; // Implement rate limiting const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5, skipSuccessfulRequests: true, }); app.post('/api/auth/login', loginLimiter, loginHandler); // Use parameterized queries const getUserByEmail = async (email) => { return User.findOne({ email }); // Protected against NoSQL injection }; // Implement CSRF protection const csrf = require('csurf'); app.use(csrf({ cookie: true })); // Set secure cookies res.cookie('token', token, { httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'strict', maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days }); // Hash passwords const bcrypt = require('bcrypt'); const hashPassword = async (password) => { return bcrypt.hash(password, 12); };
Examples
Example 1: Basic Express Microservice
const express = require('express'); const helmet = require('helmet'); const cors = require('cors'); const morgan = require('morgan'); const app = express(); // Middleware app.use(helmet()); app.use(cors()); app.use(morgan('combined')); app.use(express.json()); // Routes app.get('/health', (req, res) => { res.json({ status: 'healthy' }); }); app.get('/api/users', (req, res) => { res.json({ users: [] }); }); // Error handling app.use((err, req, res, next) => { console.error(err.stack); res.status(500).json({ error: 'Something went wrong!' }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });
Example 2: Authentication Service
const express = require('express'); const jwt = require('jsonwebtoken'); const bcrypt = require('bcrypt'); const { body, validationResult } = require('express-validator'); const router = express.Router(); // Register router.post('/register', body('email').isEmail(), body('password').isLength({ min: 8 }), async (req, res, next) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { email, password } = req.body; // Check if user exists const existingUser = await User.findOne({ email }); if (existingUser) { return res.status(409).json({ error: 'User already exists' }); } // Hash password const hashedPassword = await bcrypt.hash(password, 12); // Create user const user = await User.create({ email, password: hashedPassword, }); // Generate token const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '7d' } ); res.status(201).json({ token, user: { id: user.id, email: user.email } }); } catch (error) { next(error); } } ); // Login router.post('/login', body('email').isEmail(), body('password').exists(), async (req, res, next) => { try { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ errors: errors.array() }); } const { email, password } = req.body; // Find user const user = await User.findOne({ email }).select('+password'); if (!user) { return res.status(401).json({ error: 'Invalid credentials' }); } // Check password const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(401).json({ error: 'Invalid credentials' }); } // Generate token const token = jwt.sign( { userId: user.id }, process.env.JWT_SECRET, { expiresIn: '7d' } ); res.json({ token, user: { id: user.id, email: user.email } }); } catch (error) { next(error); } } ); module.exports = router;
Example 3: API Gateway Pattern
const express = require('express'); const { createProxyMiddleware } = require('http-proxy-middleware'); const app = express(); // Service discovery (simplified) const services = { users: 'http://users-service:3001', products: 'http://products-service:3002', orders: 'http://orders-service:3003', }; // Authentication middleware const authenticate = (req, res, next) => { const token = req.headers.authorization?.split(' ')[1]; if (!token) { return res.status(401).json({ error: 'No token provided' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); req.user = decoded; next(); } catch (error) { res.status(401).json({ error: 'Invalid token' }); } }; // Rate limiting const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100, }); app.use(limiter); // Proxy routes app.use('/api/users', authenticate, createProxyMiddleware({ target: services.users, changeOrigin: true, pathRewrite: { '^/api/users': '' }, })); app.use('/api/products', createProxyMiddleware({ target: services.products, changeOrigin: true, pathRewrite: { '^/api/products': '' }, })); app.use('/api/orders', authenticate, createProxyMiddleware({ target: services.orders, changeOrigin: true, pathRewrite: { '^/api/orders': '' }, })); // Error handling app.use((err, req, res, next) => { console.error(err); res.status(500).json({ error: 'Gateway error' }); }); const PORT = process.env.PORT || 8000; app.listen(PORT, () => { console.log(`API Gateway running on port ${PORT}`); });
See EXAMPLES.md for 15+ additional comprehensive examples including circuit breakers, event-driven patterns, service mesh integration, and more.
Skill Version: 1.0.0 Last Updated: October 2025 Skill Category: Microservices, Backend Development, Node.js, Production Architecture Compatible With: Express.js 4.x/5.x, Node.js 16+, Docker, Kubernetes