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