Express.js Integration with ScoutQuest
Learn how to integrate ScoutQuest service discovery into your Express.js applications with middleware, automatic registration, and service communication patterns.
Quick Start
Get your Express.js app connected to ScoutQuest in minutes:
Installation
npm install express scoutquest-js
Basic Setup
const express = require('express');
const { ScoutQuestClient } = require('scoutquest-js');
const app = express();
const port = process.env.PORT || 3000;
const scout = new ScoutQuestClient('http://localhost:8080');
app.use(express.json());
// Your routes here
app.get('/', (req, res) => {
res.json({ message: 'Hello from Express + ScoutQuest!' });
});
app.listen(port, async () => {
console.log(`Server running on port ${port}`);
// Register with ScoutQuest
await scout.registerService('my-express-app', 'localhost', port, {
tags: ['web', 'api'],
metadata: { framework: 'express' }
});
});
ScoutQuest Express Middleware
Create reusable middleware for common ScoutQuest operations:
middleware/scoutquest.js
const { ScoutQuestClient } = require('scoutquest-js');
class ScoutQuestMiddleware {
constructor(scoutUrl, serviceName, options = {}) {
this.client = new ScoutQuestClient(scoutUrl);
this.serviceName = serviceName;
this.options = options;
}
// Auto-register service on startup
autoRegister(host, port, serviceOptions = {}) {
return async (req, res, next) => {
if (!this.registered) {
try {
await this.client.registerService(this.serviceName, host, port, {
...this.options,
...serviceOptions
});
this.registered = true;
console.log(`✅ ${this.serviceName} registered with ScoutQuest`);
} catch (error) {
console.error('❌ Failed to register:', error);
}
}
next();
};
}
// Add ScoutQuest client to request object
injectClient() {
return (req, res, next) => {
req.scout = this.client;
next();
};
}
// Service discovery helper
serviceProxy(targetService, pathPrefix = '') {
return async (req, res, next) => {
try {
const method = req.method.toLowerCase();
const path = req.path.replace(pathPrefix, '');
const response = await this.client[method](targetService, path, req.body);
res.json(response);
} catch (error) {
res.status(error.response?.status || 500).json({
error: 'Service unavailable',
service: targetService
});
}
};
}
// Health check middleware
healthCheck(customChecks = []) {
return async (req, res) => {
const health = {
status: 'healthy',
service: this.serviceName,
timestamp: new Date().toISOString(),
checks: {}
};
// Run custom health checks
for (const check of customChecks) {
try {
health.checks[check.name] = await check.fn();
} catch (error) {
health.checks[check.name] = { status: 'unhealthy', error: error.message };
health.status = 'unhealthy';
}
}
res.status(health.status === 'healthy' ? 200 : 503).json(health);
};
}
}
module.exports = ScoutQuestMiddleware;
Complete Express App Example
Here's a full example using the middleware:
app.js
const express = require('express');
const ScoutQuestMiddleware = require('./middleware/scoutquest');
const app = express();
const port = process.env.PORT || 3000;
// Initialize ScoutQuest middleware
const scout = new ScoutQuestMiddleware(
process.env.SCOUT_URL || 'http://localhost:8080',
'user-api',
{
tags: ['api', 'users', 'express'],
metadata: {
version: '1.0.0',
framework: 'express'
}
}
);
app.use(express.json());
// Apply ScoutQuest middleware
app.use(scout.injectClient());
app.use(scout.autoRegister('localhost', port, {
healthCheck: {
path: '/health',
interval: 30
}
}));
// Health check endpoint
app.get('/health', scout.healthCheck([
{
name: 'database',
fn: async () => {
// Check database connection
return { status: 'connected' };
}
},
{
name: 'external_api',
fn: async () => {
// Check external API
return { status: 'reachable' };
}
}
]));
// Sample data
const users = [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
];
// User routes
app.get('/users', (req, res) => {
res.json(users);
});
app.get('/users/:id', (req, res) => {
const user = users.find(u => u.id === parseInt(req.params.id));
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
res.json(user);
});
app.post('/users', (req, res) => {
const newUser = {
id: users.length + 1,
name: req.body.name,
email: req.body.email
};
users.push(newUser);
res.status(201).json(newUser);
});
// Service communication example
app.get('/users/:id/orders', async (req, res) => {
try {
// Use injected ScoutQuest client
const orders = await req.scout.get('order-service', `/orders/user/${req.params.id}`);
res.json(orders);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch user orders' });
}
});
// Proxy to other services
app.use('/api/products', scout.serviceProxy('product-service', '/api/products'));
app.use('/api/orders', scout.serviceProxy('order-service', '/api/orders'));
// Service discovery endpoint
app.get('/services', async (req, res) => {
try {
const services = await req.scout.listServices();
res.json(services);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch services' });
}
});
app.listen(port, () => {
console.log(`🚀 Express server running on port ${port}`);
});
Advanced Patterns
Circuit Breaker Pattern
Implement circuit breakers for resilient service communication:
middleware/circuit-breaker.js
class CircuitBreaker {
constructor(serviceName, options = {}) {
this.serviceName = serviceName;
this.failureThreshold = options.failureThreshold || 5;
this.timeout = options.timeout || 60000;
this.resetTimeout = options.resetTimeout || 30000;
this.failures = 0;
this.state = 'CLOSED'; // CLOSED, OPEN, HALF_OPEN
this.nextAttempt = Date.now();
}
async call(fn) {
if (this.state === 'OPEN') {
if (Date.now() < this.nextAttempt) {
throw new Error(`Circuit breaker is OPEN for ${this.serviceName}`);
}
this.state = 'HALF_OPEN';
}
try {
const result = await Promise.race([
fn(),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), this.timeout)
)
]);
this.onSuccess();
return result;
} catch (error) {
this.onFailure();
throw error;
}
}
onSuccess() {
this.failures = 0;
this.state = 'CLOSED';
}
onFailure() {
this.failures++;
if (this.failures >= this.failureThreshold) {
this.state = 'OPEN';
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
}
// Usage in Express middleware
const serviceBreakers = new Map();
function withCircuitBreaker(serviceName, options = {}) {
return (req, res, next) => {
if (!serviceBreakers.has(serviceName)) {
serviceBreakers.set(serviceName, new CircuitBreaker(serviceName, options));
}
req.circuitBreaker = serviceBreakers.get(serviceName);
next();
};
}
module.exports = { CircuitBreaker, withCircuitBreaker };
Rate Limiting with Service Discovery
Rate limiting based on service capacity
const rateLimit = require('express-rate-limit');
function dynamicRateLimit(scout) {
return rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: async (req) => {
try {
// Get available instances of this service
const instances = await scout.discoverService(req.headers['x-service-name'] || 'default');
// Scale rate limit based on available instances
return Math.max(100, instances.length * 50);
} catch {
return 100; // fallback
}
},
message: 'Too many requests, please try again later.'
});
}
Testing Your Express Service
Test your Express service with ScoutQuest integration:
test/integration.test.js
const request = require('supertest');
const express = require('express');
const { ScoutQuestClient } = require('scoutquest-js');
describe('Express + ScoutQuest Integration', () => {
let app;
let scout;
beforeAll(async () => {
app = express();
scout = new ScoutQuestClient('http://localhost:8080');
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
// Register test service
await scout.registerService('test-service', 'localhost', 3001, {
tags: ['test'],
healthCheck: { path: '/health' }
});
});
afterAll(async () => {
await scout.deregisterService('test-service');
});
test('should register with ScoutQuest', async () => {
const services = await scout.listServices();
expect(services.some(s => s.name === 'test-service')).toBe(true);
});
test('health check should work', async () => {
const response = await request(app).get('/health');
expect(response.status).toBe(200);
expect(response.body.status).toBe('healthy');
});
});
Production Considerations
- Environment Variables: Use environment variables for configuration
- Graceful Shutdown: Properly deregister services on shutdown
- Error Handling: Handle service discovery failures gracefully
- Monitoring: Add metrics for service discovery operations
- Security: Secure service-to-service communication
Graceful shutdown example
process.on('SIGTERM', async () => {
console.log('🛑 Received SIGTERM, shutting down gracefully');
try {
await scout.deregisterService('my-service');
console.log('✅ Deregistered from ScoutQuest');
} catch (error) {
console.error('❌ Failed to deregister:', error);
}
process.exit(0);
});
Next Steps
- Explore the full microservices tutorial
- Learn about Rust service integration
- Set up Docker containerization
- Implement monitoring and observability