Design Patterns and Best Practices for Microservices in Node.js & Express.js
We can create scalable, independent, and flexible services with Microservices architecture. However, adopting this architecture requires following essential patterns and best practices to prevent complexity and maintainability issues.
In this blog post, we’ll discuss design patterns and best practices that are crucial for microservices architecture in Node.js and Express, providing practical examples.
Design Patterns for Microservices
1. API Gateway Pattern
The API Gateway pattern provides a single entry point for all clients, routing requests to appropriate microservices. It manages cross-cutting concerns like logging, authentication, and rate limiting.
High level implementation steps for API Gateway in Node.js
- Choose a Gateway: Decide on a tool or framework, such as Express with a reverse proxy (like
express-http-proxy
). - Create Routes: Set up routes for each microservice, mapping specific paths to their backend services.
- Implement Cross-Cutting Concerns: Add middleware for authentication, logging, and rate limiting.
- Run Gateway Server: Start the API gateway server.
Sample code for API Gateway in Node.js
const express = require('express'); const proxy = require('express-http-proxy'); const app = express(); // Proxy requests to specific microservices app.use('/api/users', proxy('http://user-service:3001')); app.use('/api/orders', proxy('http://order-service:3002')); // Start the API gateway server app.listen(3000, () => { console.log('API Gateway running on port 3000'); });
2. Circuit Breaker Pattern
The circuit breaker pattern prevents cascading failures by stopping requests to services that are down and switching to a fallback mechanism.
High level implementation steps for Circuit Breaker in Node.js
- Choose a Circuit Breaker Library: Pick a library like
opossum
or implement your own. - Define Remote Requests: Specify the service requests that could fail.
- Configure Circuit Breaker: Set error thresholds, timeouts, and fallback functions.
- Monitor Status: Track circuit status and log fallback calls.
Sample code for Circuit Breaker in Node.js
const CircuitBreaker = require('opossum'); const axios = require('axios'); const request = () => axios.get('http://unstable-service:3001'); const breaker = new CircuitBreaker(request, { timeout: 5000, // 5-second timeout errorThresholdPercentage: 50, // Trigger if error rate exceeds 50% resetTimeout: 30000 // Reset after 30 seconds }); breaker.fallback(() => ({ message: 'Service temporarily unavailable, please try later.' })); breaker.on('fallback', () => console.log('Circuit breaker tripped - fallback called')); breaker.fire() .then(console.log) .catch(console.error);
3. Saga Pattern
The Saga pattern coordinates distributed transactions across multiple services by managing local transactions and passing results through events or messages.
High level implementation steps for Saga Pattern in Node.js
- Select an Event Broker: Choose a message broker like Kafka or RabbitMQ.
- Define Events: Create a standardized set of events that will be published between services.
- Develop Producers: Implement services that produce events after successful transactions.
- Develop Consumers: Implement services that consume these events and perform their respective tasks.
- Handle Failures: Implement compensating transactions or rollback strategies.
Sample code for Saga implementation in Node.js
// Service A (Producer) const kafka = require('kafka-node'); const producer = new kafka.Producer(new kafka.KafkaClient({ kafkaHost: 'localhost:9092' })); producer.on('ready', () => { const event = { userId: 123, action: 'UserCreated' }; producer.send([{ topic: 'user-events', messages: JSON.stringify(event) }], (err, data) => { if (err) console.error(err); else console.log('Event published:', data); }); }); // Service B (Consumer) const kafka = require('kafka-node'); const consumer = new kafka.Consumer( new kafka.KafkaClient({ kafkaHost: 'localhost:9092' }), [{ topic: 'user-events', partition: 0 }] ); consumer.on('message', (message) => { const event = JSON.parse(message.value); console.log('Received event:', event); if (event.action === 'UserCreated') { // Perform a corresponding operation for this event } });
4. Strangler Fig Pattern
The Strangler Fig pattern is used when migrating legacy monolithic applications to microservices. New functionality is implemented as microservices while the old system remains intact. Over time, more features migrate to microservices until the legacy system is fully replaced.
Highlevel steps
- Identify Features to Migrate: Determine the legacy features that should be rewritten.
- Implement New Features: Develop new microservices for these features.
- Set Up API Gateway: Route new feature requests to the microservices and legacy requests to the old system.
- Test and Migrate: Gradually migrate old features until the legacy system can be safely deprecated.
Sample code for implementation in Node.js
// Import proxy middleware and initialize an API Gateway const express = require('express'); const proxy = require('express-http-proxy'); const app = express(); // Route new features to microservices app.use('/api/v2/users', proxy('http://user-service:3001')); app.use('/api/v2/orders', proxy('http://order-service:3002')); // Fallback: Route old features to the legacy system app.use('/api/v1', proxy('http://legacy-system:3000')); // Start the API Gateway server app.listen(3000, () => { console.log('API Gateway running on port 3000'); });
5. Event Sourcing Pattern
The Event Sourcing pattern captures all changes to application state as events in an event store. This allows replaying events to reconstruct the state.
- Create an Event Store: Select a database or broker to store events.
- Design Event Structure: Define a consistent format for events, including metadata.
- Publish Events: Implement producers to create events for every change in state.
- Replay Events: Develop a mechanism to replay events to reconstruct the current state.
- Update Read Models: Create read models (views or projections) for different aggregates.
Sample code for implementation
// Simple in-memory event store (for demonstration purposes) const eventStore = []; function addEvent(event) { eventStore.push(event); } function replayEvents() { const state = {}; eventStore.forEach(event => { switch (event.type) { case 'USER_CREATED': state[event.userId] = { ...event.payload }; break; case 'USER_UPDATED': Object.assign(state[event.userId], event.payload); break; case 'USER_DELETED': delete state[event.userId]; break; default: console.warn('Unknown event type:', event.type); } }); return state; } // Example usage: Adding events addEvent({ type: 'USER_CREATED', userId: '123', payload: { name: 'Alice', age: 30 } }); addEvent({ type: 'USER_UPDATED', userId: '123', payload: { age: 31 } }); addEvent({ type: 'USER_DELETED', userId: '123' }); // Rebuild state from events const currentState = replayEvents(); console.log('Rebuilt State:', currentState);
Best Practices for Microservices Development
- Decentralized Data Management: Each service should have its own database/bounded context to maintain autonomy and independence from other services.
- Statelessness: Avoid storing client session information directly within services. Use tokens or session storage to scale horizontally.
- Service Discovery: Implement mechanisms like Consul or Eureka to dynamically register and locate services.
- Logging and Monitoring: Centralize logging and monitoring to analyze service health and performance, using tools like Prometheus, Grafana, or ELK Stack.
- API Versioning: Ensure backward compatibility by versioning APIs and preventing breaking changes to existing clients.
- Security: Implement robust authentication and authorization using OAuth, JWT, or API keys.
- Rate Limiting: Apply rate limiting to APIs to protect against abuse and prevent Denial of Service (DoS) attacks.
Conclusion
By following these patterns and best practices, you can design robust, scalable, and efficient microservices. Node.js and Express are powerful tools that can help you implement them effectively, providing the flexibility needed to create resilient and modular applications.
© 2024, https:. All rights reserved. On republishing this post, you must provide link to original post