@develop-x/nest-consul v1.0.3
@develop-x/nest-consul
Overview
@develop-x/nest-consul
is a NestJS package that provides Consul integration for service discovery and key-value store operations. It enables microservices to register themselves, discover other services, and manage configuration data through Consul's distributed system.
Installation
npm install @develop-x/nest-consul
Features
- Service Registration: Automatic service registration with Consul
- Service Discovery: Find and connect to other services
- Health Checks: Built-in health check endpoints
- Key-Value Store: Manage configuration and shared data
- Load Balancing: Automatic load balancing across service instances
- Configuration Management: Centralized configuration storage
- TypeScript Support: Full TypeScript support with proper typing
Usage
Module Import
Import the ConsulModule
in your application module:
import { Module } from '@nestjs/common';
import { ConsulModule } from '@develop-x/nest-consul';
@Module({
imports: [
ConsulModule.forRoot('http://consul:8500'), // Consul server URL
],
})
export class AppModule {}
Service Registration
Register your service with Consul:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { ConsulService } from '@develop-x/nest-consul';
@Injectable()
export class AppService implements OnModuleInit {
constructor(private readonly consulService: ConsulService) {}
async onModuleInit() {
await this.consulService.registerService({
name: 'user-service',
id: 'user-service-1',
address: 'localhost',
port: 3000,
tags: ['api', 'users', 'v1'],
check: {
http: 'http://localhost:3000/health',
interval: '10s',
timeout: '5s',
},
});
}
}
Service Discovery
Discover and connect to other services:
import { Injectable } from '@nestjs/common';
import { ConsulService } from '@develop-x/nest-consul';
@Injectable()
export class UserService {
constructor(private readonly consulService: ConsulService) {}
async getOrderServiceUrl(): Promise<string> {
const services = await this.consulService.getHealthyServices('order-service');
if (services.length === 0) {
throw new Error('Order service not available');
}
// Simple round-robin load balancing
const service = services[Math.floor(Math.random() * services.length)];
return `http://${service.address}:${service.port}`;
}
async callOrderService(userId: string) {
const orderServiceUrl = await this.getOrderServiceUrl();
// Make HTTP request to order service
const response = await fetch(`${orderServiceUrl}/orders?userId=${userId}`);
return response.json();
}
}
API Reference
ConsulModule Configuration
forRoot(consulUrl: string)
Configure the Consul module with the Consul server URL.
Parameters:
consulUrl: string
- Consul server URL (e.g., 'http://localhost:8500')
ConsulService
registerService(serviceConfig: ServiceConfig)
Register a service with Consul.
Parameters:
interface ServiceConfig {
name: string; // Service name
id: string; // Unique service instance ID
address: string; // Service address/hostname
port: number; // Service port
tags?: string[]; // Service tags for filtering
meta?: Record<string, string>; // Service metadata
check?: HealthCheck; // Health check configuration
}
interface HealthCheck {
http?: string; // HTTP health check URL
tcp?: string; // TCP health check address:port
interval: string; // Check interval (e.g., '10s')
timeout: string; // Check timeout (e.g., '5s')
deregisterCriticalServiceAfter?: string; // Auto-deregister after
}
getHealthyServices(serviceName: string)
Get all healthy instances of a service.
Parameters:
serviceName: string
- Name of the service to discover
Returns: Promise<ServiceInstance[]>
interface ServiceInstance {
id: string;
name: string;
address: string;
port: number;
tags: string[];
meta: Record<string, string>;
}
deregisterService(serviceId: string)
Deregister a service from Consul.
Parameters:
serviceId: string
- Unique service instance ID
KvStoreService
set(key: string, value: any)
Store a value in Consul's key-value store.
await this.kvStoreService.set('config/database/url', 'postgresql://...');
await this.kvStoreService.set('config/features', { featureA: true, featureB: false });
get(key: string)
Retrieve a value from Consul's key-value store.
const dbUrl = await this.kvStoreService.get<string>('config/database/url');
const features = await this.kvStoreService.get<FeatureFlags>('config/features');
delete(key: string)
Delete a key from Consul's key-value store.
await this.kvStoreService.delete('config/deprecated-setting');
getKeys(prefix: string)
Get all keys with a specific prefix.
const configKeys = await this.kvStoreService.getKeys('config/');
Advanced Usage
Health Check Endpoint
Create a health check endpoint for your service:
import { Controller, Get } from '@nestjs/common';
@Controller('health')
export class HealthController {
@Get()
getHealth() {
return {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
};
}
}
Service Discovery with Load Balancing
Implement advanced load balancing strategies:
import { Injectable } from '@nestjs/common';
import { ConsulService } from '@develop-x/nest-consul';
@Injectable()
export class LoadBalancedService {
private serviceCounters = new Map<string, number>();
constructor(private readonly consulService: ConsulService) {}
async getServiceUrl(serviceName: string, strategy: 'round-robin' | 'random' = 'round-robin'): Promise<string> {
const services = await this.consulService.getHealthyServices(serviceName);
if (services.length === 0) {
throw new Error(`No healthy instances of ${serviceName} found`);
}
let selectedService;
switch (strategy) {
case 'round-robin':
const counter = this.serviceCounters.get(serviceName) || 0;
selectedService = services[counter % services.length];
this.serviceCounters.set(serviceName, counter + 1);
break;
case 'random':
default:
selectedService = services[Math.floor(Math.random() * services.length)];
break;
}
return `http://${selectedService.address}:${selectedService.port}`;
}
}
Configuration Management
Use Consul KV store for centralized configuration:
import { Injectable, OnModuleInit } from '@nestjs/common';
import { KvStoreService } from '@develop-x/nest-consul';
@Injectable()
export class ConfigService implements OnModuleInit {
private config: any = {};
constructor(private readonly kvStore: KvStoreService) {}
async onModuleInit() {
await this.loadConfiguration();
}
private async loadConfiguration() {
try {
// Load database configuration
this.config.database = await this.kvStore.get('config/database');
// Load feature flags
this.config.features = await this.kvStore.get('config/features');
// Load API keys
this.config.apiKeys = await this.kvStore.get('config/api-keys');
console.log('Configuration loaded from Consul');
} catch (error) {
console.error('Failed to load configuration from Consul:', error);
// Use default configuration or environment variables as fallback
}
}
get<T>(key: string): T {
return this.getNestedValue(this.config, key);
}
private getNestedValue(obj: any, path: string): any {
return path.split('.').reduce((current, key) => current?.[key], obj);
}
async updateConfig(key: string, value: any) {
await this.kvStore.set(`config/${key}`, value);
await this.loadConfiguration(); // Reload configuration
}
}
Service Mesh Integration
Integrate with service mesh patterns:
import { Injectable } from '@nestjs/common';
import { ConsulService } from '@develop-x/nest-consul';
@Injectable()
export class ServiceMeshService {
constructor(private readonly consulService: ConsulService) {}
async registerWithMesh(serviceConfig: {
name: string;
version: string;
protocol: 'http' | 'grpc';
endpoints: string[];
}) {
await this.consulService.registerService({
name: serviceConfig.name,
id: `${serviceConfig.name}-${serviceConfig.version}-${Date.now()}`,
address: process.env.SERVICE_HOST || 'localhost',
port: parseInt(process.env.SERVICE_PORT || '3000'),
tags: [
`version:${serviceConfig.version}`,
`protocol:${serviceConfig.protocol}`,
...serviceConfig.endpoints.map(ep => `endpoint:${ep}`),
],
meta: {
version: serviceConfig.version,
protocol: serviceConfig.protocol,
endpoints: serviceConfig.endpoints.join(','),
},
check: {
http: `http://${process.env.SERVICE_HOST || 'localhost'}:${process.env.SERVICE_PORT || '3000'}/health`,
interval: '10s',
timeout: '5s',
deregisterCriticalServiceAfter: '30s',
},
});
}
async discoverServicesByTag(tag: string) {
const allServices = await this.consulService.getHealthyServices('');
return allServices.filter(service => service.tags.includes(tag));
}
}
Multi-Environment Configuration
Handle different environments with Consul:
import { Injectable } from '@nestjs/common';
import { KvStoreService } from '@develop-x/nest-consul';
@Injectable()
export class EnvironmentConfigService {
private environment = process.env.NODE_ENV || 'development';
constructor(private readonly kvStore: KvStoreService) {}
async getEnvironmentConfig<T>(configKey: string): Promise<T> {
const envKey = `config/${this.environment}/${configKey}`;
try {
return await this.kvStore.get<T>(envKey);
} catch (error) {
// Fallback to default configuration
const defaultKey = `config/default/${configKey}`;
return await this.kvStore.get<T>(defaultKey);
}
}
async setEnvironmentConfig(configKey: string, value: any) {
const envKey = `config/${this.environment}/${configKey}`;
await this.kvStore.set(envKey, value);
}
}
Integration Examples
With Docker and Docker Compose
# docker-compose.yml
version: '3.8'
services:
consul:
image: consul:latest
ports:
- "8500:8500"
command: consul agent -dev -client=0.0.0.0
user-service:
build: ./user-service
environment:
- CONSUL_URL=http://consul:8500
- SERVICE_NAME=user-service
- SERVICE_PORT=3000
depends_on:
- consul
ports:
- "3000:3000"
order-service:
build: ./order-service
environment:
- CONSUL_URL=http://consul:8500
- SERVICE_NAME=order-service
- SERVICE_PORT=3001
depends_on:
- consul
ports:
- "3001:3001"
With Kubernetes
# consul-service.yaml
apiVersion: v1
kind: Service
metadata:
name: consul
spec:
selector:
app: consul
ports:
- port: 8500
targetPort: 8500
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: consul
spec:
replicas: 1
selector:
matchLabels:
app: consul
template:
metadata:
labels:
app: consul
spec:
containers:
- name: consul
image: consul:latest
args: ["consul", "agent", "-dev", "-client=0.0.0.0"]
ports:
- containerPort: 8500
Graceful Shutdown
Handle graceful service deregistration:
import { Injectable, OnApplicationShutdown } from '@nestjs/common';
import { ConsulService } from '@develop-x/nest-consul';
@Injectable()
export class AppService implements OnApplicationShutdown {
private serviceId: string;
constructor(private readonly consulService: ConsulService) {}
async onModuleInit() {
this.serviceId = `user-service-${Date.now()}`;
await this.consulService.registerService({
name: 'user-service',
id: this.serviceId,
address: 'localhost',
port: 3000,
check: {
http: 'http://localhost:3000/health',
interval: '10s',
timeout: '5s',
},
});
}
async onApplicationShutdown(signal?: string) {
console.log(`Received shutdown signal: ${signal}`);
try {
await this.consulService.deregisterService(this.serviceId);
console.log('Service deregistered from Consul');
} catch (error) {
console.error('Failed to deregister service:', error);
}
}
}
Testing
Unit Testing
import { Test, TestingModule } from '@nestjs/testing';
import { ConsulService } from '@develop-x/nest-consul';
import { UserService } from './user.service';
describe('UserService', () => {
let service: UserService;
let consulService: ConsulService;
beforeEach(async () => {
const mockConsulService = {
getHealthyServices: jest.fn(),
registerService: jest.fn(),
deregisterService: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UserService,
{
provide: ConsulService,
useValue: mockConsulService,
},
],
}).compile();
service = module.get<UserService>(UserService);
consulService = module.get<ConsulService>(ConsulService);
});
it('should get order service URL', async () => {
const mockServices = [
{ address: 'localhost', port: 3001, id: 'order-1', name: 'order-service', tags: [], meta: {} },
];
(consulService.getHealthyServices as jest.Mock).mockResolvedValue(mockServices);
const url = await service.getOrderServiceUrl();
expect(url).toBe('http://localhost:3001');
expect(consulService.getHealthyServices).toHaveBeenCalledWith('order-service');
});
});
Integration Testing
import { Test, TestingModule } from '@nestjs/testing';
import { ConsulModule } from '@develop-x/nest-consul';
import { UserService } from './user.service';
describe('UserService Integration', () => {
let service: UserService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConsulModule.forRoot('http://localhost:8500'), // Test Consul instance
],
providers: [UserService],
}).compile();
service = module.get<UserService>(UserService);
});
it('should discover services from Consul', async () => {
// This test requires a running Consul instance
const url = await service.getOrderServiceUrl();
expect(url).toMatch(/^http:\/\/.+:\d+$/);
});
});
Best Practices
- Service Naming: Use consistent and descriptive service names
- Health Checks: Always implement proper health check endpoints
- Graceful Shutdown: Properly deregister services on shutdown
- Error Handling: Handle service discovery failures gracefully
- Load Balancing: Implement appropriate load balancing strategies
- Configuration: Use Consul KV store for centralized configuration
- Monitoring: Monitor service health and registration status
- Security: Secure Consul access in production environments
Security Considerations
- ACL Tokens: Use Consul ACL tokens for authentication
- TLS Encryption: Enable TLS for Consul communication
- Network Security: Restrict network access to Consul
- Sensitive Data: Encrypt sensitive configuration data
Troubleshooting
Common Issues
- Service Not Found: Check service registration and health status
- Connection Refused: Verify Consul server is running and accessible
- Health Check Failures: Ensure health check endpoints are working
- Load Balancing Issues: Verify multiple service instances are registered
Debug Mode
Enable debug logging for troubleshooting:
// Set environment variable
process.env.DEBUG = 'consul:*';
// Or use console logging
console.log('Consul services:', await consulService.getHealthyServices('user-service'));
Dependencies
@nestjs/common
: NestJS common utilities@nestjs/core
: NestJS core functionality
License
ISC
Support
For issues and questions, please refer to the project repository or contact the development team.