1.0.3 • Published 5 months ago

@develop-x/nest-consul v1.0.3

Weekly downloads
-
License
ISC
Repository
-
Last release
5 months ago

@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:

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

  1. Service Naming: Use consistent and descriptive service names
  2. Health Checks: Always implement proper health check endpoints
  3. Graceful Shutdown: Properly deregister services on shutdown
  4. Error Handling: Handle service discovery failures gracefully
  5. Load Balancing: Implement appropriate load balancing strategies
  6. Configuration: Use Consul KV store for centralized configuration
  7. Monitoring: Monitor service health and registration status
  8. Security: Secure Consul access in production environments

Security Considerations

  1. ACL Tokens: Use Consul ACL tokens for authentication
  2. TLS Encryption: Enable TLS for Consul communication
  3. Network Security: Restrict network access to Consul
  4. Sensitive Data: Encrypt sensitive configuration data

Troubleshooting

Common Issues

  1. Service Not Found: Check service registration and health status
  2. Connection Refused: Verify Consul server is running and accessible
  3. Health Check Failures: Ensure health check endpoints are working
  4. 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.

1.0.3

5 months ago

1.0.2

5 months ago

1.0.1

5 months ago

1.0.0

5 months ago