Gateway Package

npm downloads

The @hazeljs/gateway package provides an intelligent API gateway for HazelJS microservices. It handles version-based routing, canary deployments with automatic rollback, circuit breaking, rate limiting, traffic mirroring, and request transformation — all configurable via environment variables or decorators.

Purpose

An API gateway is the single entry point for your microservices. Instead of clients knowing about individual service URLs, they talk to the gateway, which routes requests to the right service. The @hazeljs/gateway package solves this by providing:

  • Config-Driven Routes: Define all routes from env vars via @hazeljs/config — no redeployment needed to change behavior
  • Canary Deployments: Gradually shift traffic to new versions, monitor error rates, and automatically promote or rollback
  • Version Routing: Route by X-API-Version header, URI prefix, query parameter, or weighted random
  • Circuit Breaker: Per-route circuit breaker protection via @hazeljs/resilience
  • Rate Limiting: Per-route rate limits to protect downstream services
  • Traffic Mirroring: Shadow traffic to test new versions without affecting production
  • Request Transformation: Modify requests and responses in transit
  • Real-Time Metrics: Per-route and per-version performance tracking

Architecture

The gateway sits between clients and your services, applying routing, resilience, and traffic management policies:

graph TD
  A["Client Request"] --> B["GatewayServer"]
  B --> C["Route Matcher"]
  C --> D{"Has Canary?"}
  D -->|Yes| E["CanaryEngine<br/>(Version Selection)"]
  D -->|No| F{"Has Version Route?"}
  F -->|Yes| G["VersionRouter<br/>(Header/URI/Query)"]
  F -->|No| H["ServiceProxy"]
  E --> H
  G --> H
  H --> I["CircuitBreaker + RateLimit"]
  I --> J["DiscoveryClient<br/>(Load Balanced)"]
  J --> K["Upstream Service"]
  
  style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style B fill:#6366f1,stroke:#818cf8,stroke-width:2px,color:#fff
  style E fill:#f59e0b,stroke:#fbbf24,stroke-width:2px,color:#fff
  style G fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff
  style H fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style I fill:#ef4444,stroke:#f87171,stroke-width:2px,color:#fff
  style K fill:#ec4899,stroke:#f472b6,stroke-width:2px,color:#fff

Key Components

  1. GatewayServer: The main orchestrator — matches routes, applies policies, and forwards requests
  2. ServiceProxy: HTTP client that resolves service instances via discovery and applies resilience patterns
  3. CanaryEngine: Manages canary deployment lifecycle — traffic weights, metric evaluation, promotion steps, and rollback
  4. VersionRouter: Resolves target version from headers, URI paths, query params, or weighted distribution
  5. GatewayMetrics: Per-route and per-version metrics collection
  6. TrafficMirror: Sends shadow traffic to test services
  7. GatewayModule: Integration with @hazeljs/config for config-driven routing

Installation

npm install @hazeljs/gateway

The gateway depends on @hazeljs/discovery and @hazeljs/resilience (installed automatically). For config-driven routing, also install:

npm install @hazeljs/config

Quick Start — Config-Driven (Recommended)

The config-driven approach reads all gateway settings from environment variables, so you can change routing behavior, canary weights, circuit breaker thresholds, and more without touching code.

1. Create a Config Loader

Create a gateway.config.ts file that reads from env vars:

const gatewayConfig = () => ({
  gateway: {
    discovery: {
      cacheEnabled: process.env.GATEWAY_CACHE_ENABLED !== 'false',
      cacheTTL: parseInt(process.env.GATEWAY_CACHE_TTL || '30000'),
    },
    resilience: {
      defaultCircuitBreaker: {
        failureThreshold: parseInt(process.env.GATEWAY_CB_THRESHOLD || '5'),
        resetTimeout: parseInt(process.env.GATEWAY_CB_RESET_TIMEOUT || '30000'),
      },
      defaultTimeout: parseInt(process.env.GATEWAY_DEFAULT_TIMEOUT || '5000'),
    },
    routes: [
      {
        path: '/api/users/**',
        serviceName: 'user-service',
        serviceConfig: { stripPrefix: '/api/users', addPrefix: '/users' },
        circuitBreaker: {
          failureThreshold: parseInt(process.env.USER_SVC_CB_THRESHOLD || '10'),
        },
        rateLimit: {
          strategy: 'sliding-window',
          max: parseInt(process.env.USER_SVC_RATE_LIMIT_MAX || '100'),
          window: 60000,
        },
      },
      {
        path: '/api/orders/**',
        serviceName: 'order-service',
        canary: {
          stable: {
            version: process.env.ORDER_STABLE_VERSION || 'v1',
            weight: parseInt(process.env.ORDER_STABLE_WEIGHT || '90'),
          },
          canary: {
            version: process.env.ORDER_CANARY_VERSION || 'v2',
            weight: parseInt(process.env.ORDER_CANARY_WEIGHT || '10'),
          },
          promotion: {
            strategy: 'error-rate',
            errorThreshold: parseInt(process.env.ORDER_CANARY_ERROR_THRESHOLD || '5'),
            evaluationWindow: '5m',
            autoPromote: true,
            autoRollback: true,
            steps: [10, 25, 50, 75, 100],
            stepInterval: '10m',
          },
        },
      },
    ],
  },
});

export default gatewayConfig;

2. Wire It Up

import { ConfigModule, ConfigService } from '@hazeljs/config';
import { GatewayServer, GatewayModule } from '@hazeljs/gateway';
import gatewayConfig from './gateway.config';

// Register the config loader
ConfigModule.forRoot({
  envFilePath: ['.env', '.env.local'],
  isGlobal: true,
  load: [gatewayConfig],
});

// Register gateway module
GatewayModule.forRoot({ configKey: 'gateway' });

// Resolve config and create gateway
const configService = new ConfigService();
const config = GatewayModule.resolveConfig(configService);
const gateway = GatewayServer.fromConfig(config);

// Start canary evaluation loops
gateway.startCanaries();

3. Set Env Vars Per Environment

# .env (development)
ORDER_CANARY_VERSION=v2
ORDER_CANARY_ERROR_THRESHOLD=5
USER_SVC_RATE_LIMIT_MAX=100

# .env.production
ORDER_CANARY_VERSION=v3
ORDER_CANARY_ERROR_THRESHOLD=2
USER_SVC_RATE_LIMIT_MAX=500

Environment Variable Convention

PrefixScopeExample
GATEWAY_*Global gateway settingsGATEWAY_DEFAULT_TIMEOUT=5000
GATEWAY_CB_*Default circuit breakerGATEWAY_CB_THRESHOLD=5
<SERVICE>_SVC_*Per-service overridesUSER_SVC_RATE_LIMIT_MAX=100
<SERVICE>_CANARY_*Canary deploymentORDER_CANARY_ERROR_THRESHOLD=5

Decorator API

Decorators remain available for quick prototypes and when you prefer co-located configuration:

import {
  Gateway, Route, ServiceRoute, Canary, VersionRoute,
  GatewayCircuitBreaker, GatewayRateLimit, TrafficPolicy,
  GatewayServer,
} from '@hazeljs/gateway';

@Gateway({
  resilience: { defaultCircuitBreaker: { failureThreshold: 5 } },
  metrics: { enabled: true, collectionInterval: '10s' },
})
class ApiGateway {
  @Route('/api/users/**')
  @ServiceRoute({
    serviceName: 'user-service',
    stripPrefix: '/api/users',
    addPrefix: '/users',
  })
  @GatewayCircuitBreaker({ failureThreshold: 10 })
  @GatewayRateLimit({ strategy: 'sliding-window', max: 100, window: 60000 })
  userService!: ServiceProxy;

  @Route('/api/orders/**')
  @ServiceRoute({ serviceName: 'order-service' })
  @Canary({
    stable: { version: 'v1', weight: 90 },
    canary: { version: 'v2', weight: 10 },
    promotion: {
      strategy: 'error-rate',
      errorThreshold: 5,
      evaluationWindow: '5m',
      autoPromote: true,
      autoRollback: true,
      steps: [10, 25, 50, 75, 100],
      stepInterval: '10m',
    },
  })
  orderService!: ServiceProxy;
}

const gateway = GatewayServer.fromClass(ApiGateway);
gateway.startCanaries();

Canary Deployments

Canary deployments let you gradually shift traffic from a stable version to a new version while monitoring for errors. If the new version is healthy, it gets promoted. If not, traffic rolls back automatically.

How It Works

graph LR
  A["Deploy v2"] --> B["10% canary"]
  B --> C{"Healthy?"}
  C -->|"errors < 5%"| D["25%"]
  D --> E["50%"]
  E --> F["75%"]
  F --> G["100% ✓"]
  C -->|"errors > 5%"| H["Rollback 0% ✗"]
  
  style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style B fill:#f59e0b,stroke:#fbbf24,stroke-width:2px,color:#fff
  style G fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style H fill:#ef4444,stroke:#f87171,stroke-width:2px,color:#fff

Promotion Strategies

StrategyMetricUse Case
error-ratePercentage of 5xx responsesMost common — catches bugs and crashes
latencyp99 or average latency thresholdCatches performance regressions
customYour own evaluation functionComplex multi-metric decisions

Configuration

{
  canary: {
    stable: { version: 'v1', weight: 90 },
    canary: { version: 'v2', weight: 10 },
    promotion: {
      strategy: 'error-rate',
      errorThreshold: 5,              // Rollback if errors > 5%
      evaluationWindow: '5m',         // Evaluate over 5 minutes
      autoPromote: true,              // Automatically advance through steps
      autoRollback: true,             // Automatically rollback on threshold breach
      steps: [10, 25, 50, 75, 100],  // Weight progression
      stepInterval: '10m',            // Time between steps
      minRequests: 10,                // Wait for enough data before deciding
    },
  },
}

Events

Listen to canary lifecycle events:

gateway.on('canary:promote', (data) => {
  console.log(`Step ${data.step}/${data.totalSteps}: canary at ${data.canaryWeight}%`);
});

gateway.on('canary:rollback', (data) => {
  console.log(`Rolled back: ${data.canaryVersion} -> ${data.stableVersion}`);
  console.log(`Trigger: ${data.trigger}`);
});

gateway.on('canary:complete', (data) => {
  console.log(`${data.version} is now receiving 100% traffic`);
});

Manual Control

const engine = gateway.getCanaryEngine('/api/orders/**');

// Pause automatic promotion
engine.pause();

// Resume
engine.resume();

// Force promote to next step
engine.promote();

// Force rollback
engine.rollback();

Version Routing

Route requests to specific service versions based on different strategies.

Strategies

StrategyHow It WorksExample
HeaderClient sends X-API-Version: v2Opt-in for specific clients
URIPath prefix /v2/api/usersRESTful versioning
Query?version=v2Quick testing
WeightedPercentage-based randomA/B testing

Configuration

{
  versionRoute: {
    strategy: 'header',                    // 'header' | 'uri' | 'query'
    header: 'X-API-Version',              // Header name (for header strategy)
    defaultVersion: 'v1',                  // Default when no version specified
    routes: {
      v1: { weight: 100 },                // All default traffic goes to v1
      v2: { weight: 0, allowExplicit: true }, // Only when explicitly requested
    },
  },
}

Weighted Routing for A/B Tests

{
  versionRoute: {
    routes: {
      v1: { weight: 80 },    // 80% of traffic
      v2: { weight: 20 },    // 20% of traffic (A/B test)
    },
  },
}

Traffic Mirroring

Mirror a percentage of traffic to a test service without affecting the response sent to the client. The mirrored request is fire-and-forget.

{
  trafficPolicy: {
    mirror: {
      service: 'search-v2',       // Target service for shadow traffic
      percentage: 10,              // Mirror 10% of requests
    },
    timeout: 3000,
    retry: { maxAttempts: 2, backoff: 'exponential', baseDelay: 500 },
  },
}

This is useful for:

  • Testing a new version with real production traffic
  • Comparing responses between versions
  • Performance benchmarking under realistic load

Route Matching

The gateway supports glob patterns, path parameters, and wildcards:

PatternMatchesExample
/api/users/**Any path under /api/users//api/users/123/orders
/api/users/:idSingle path parameter/api/users/123
/api/users/*Single segment wildcard/api/users/123 but not /api/users/123/orders
/api/usersExact matchOnly /api/users

Routes are automatically sorted by specificity — more specific patterns match first.

Programmatic API

For full control, use the GatewayServer class directly:

import { GatewayServer } from '@hazeljs/gateway';

const gateway = new GatewayServer({
  discovery: { cacheEnabled: true },
  resilience: { defaultTimeout: 5000 },
  metrics: { enabled: true },
});

// Add routes programmatically
gateway.addRoute({
  path: '/api/users/**',
  serviceName: 'user-service',
  serviceConfig: { stripPrefix: '/api/users', addPrefix: '/users' },
  circuitBreaker: { failureThreshold: 5 },
});

gateway.addRoute({
  path: '/api/orders/**',
  serviceName: 'order-service',
  canary: {
    stable: { version: 'v1', weight: 90 },
    canary: { version: 'v2', weight: 10 },
    promotion: {
      strategy: 'error-rate',
      errorThreshold: 5,
      evaluationWindow: '5m',
      autoPromote: true,
      autoRollback: true,
      steps: [10, 25, 50, 75, 100],
      stepInterval: '10m',
    },
  },
});

// Handle a request
const response = await gateway.handleRequest({
  method: 'GET',
  path: '/api/users/123',
  headers: {},
});

// Start canaries and clean up
gateway.startCanaries();
process.on('SIGTERM', () => gateway.stop());

Integrating with HazelApp

Use the gateway with HazelApp's built-in HTTP server via addProxyHandler and createGatewayHandler. Requests matching the path prefix are forwarded to the gateway before the router:

import { HazelApp } from '@hazeljs/core';
import { GatewayServer, createGatewayHandler } from '@hazeljs/gateway';

const gateway = GatewayServer.fromConfig(config);
gateway.startCanaries();

const app = new HazelApp(AppModule);
app.addProxyHandler('/api', createGatewayHandler(gateway));
app.listen(3000);

addProxyHandler(pathPrefix, handler) runs after body parsing and before the router. This lets you serve both gateway-proxied routes (e.g. /api/*) and regular HazelJS controllers (e.g. /health) from the same server.

Metrics

The gateway collects per-route and per-version metrics in real time:

const metrics = gateway.getMetrics();
const snapshot = metrics.getSnapshot();

console.log(snapshot);
// {
//   routes: {
//     '/api/users/**': {
//       totalRequests: 5000,
//       successCount: 4950,
//       failureCount: 50,
//       failureRate: 1,
//       averageLatency: 35,
//       p99Latency: 120,
//       versions: { ... },
//     },
//     '/api/orders/**': {
//       totalRequests: 3000,
//       versions: {
//         'v1': { totalRequests: 2700, failureRate: 0.5 },
//         'v2': { totalRequests: 300, failureRate: 2.1 },
//       },
//     },
//   },
// }

These metrics feed into the canary engine's promotion and rollback decisions.

GatewayModule

The GatewayModule integrates with @hazeljs/config and follows the same forRoot() pattern:

import { GatewayModule, GatewayServer } from '@hazeljs/gateway';
import { ConfigService } from '@hazeljs/config';

// Option 1: Config-driven (reads from ConfigService)
GatewayModule.forRoot({ configKey: 'gateway' });

const configService = new ConfigService();
const config = GatewayModule.resolveConfig(configService);
const gateway = GatewayServer.fromConfig(config);

// Option 2: Direct config (no ConfigService needed)
GatewayModule.forRoot({
  config: {
    routes: [
      { path: '/api/users/**', serviceName: 'user-service' },
    ],
  },
});
const config = GatewayModule.resolveConfig();
const gateway = GatewayServer.fromConfig(config);

Complete Example

Here is a full production-like setup combining config-driven routing with canary deployments:

import { HazelApp, HazelModule, Controller, Get, Injectable } from '@hazeljs/core';
import { ConfigModule, ConfigService } from '@hazeljs/config';
import { ServiceRegistry } from '@hazeljs/discovery';
import { GatewayServer, GatewayModule } from '@hazeljs/gateway';
import gatewayConfig from './gateway.config';

// Health check controller
@Controller('/health')
@Injectable()
class HealthController {
  @Get('/')
  async check() {
    return { status: 'UP', timestamp: new Date().toISOString() };
  }
}

async function startGateway() {
  // 1. Load config from env vars
  ConfigModule.forRoot({
    envFilePath: ['.env', '.env.local'],
    isGlobal: true,
    load: [gatewayConfig],
  });

  // 2. Create gateway from config
  GatewayModule.forRoot({ configKey: 'gateway' });
  const configService = new ConfigService();
  const config = GatewayModule.resolveConfig(configService);
  const gateway = GatewayServer.fromConfig(config);

  // 3. Listen to events
  gateway.on('canary:promote', (data) => {
    console.log(`Canary promoted: step ${data.step}, weight ${data.canaryWeight}%`);
  });
  gateway.on('canary:rollback', (data) => {
    console.log(`Canary rolled back: ${data.trigger}`);
  });

  // 4. Start
  gateway.startCanaries();

  @HazelModule({ controllers: [HealthController] })
  class GatewayAppModule {}

  const app = new HazelApp(GatewayAppModule);
  await app.listen(parseInt(process.env.GATEWAY_PORT || '3003'));

  console.log('Gateway routes:', gateway.getRoutes());
}

startGateway();

Best Practices

  1. Use config-driven routes in production: Decorators are convenient for prototyping, but config-driven routes let you change behavior without redeploying.

  2. Start with conservative canary weights: Begin with 5-10% canary traffic. You can always increase via env vars.

  3. Set appropriate evaluation windows: Too short and you don't have enough data. Too long and you're exposing users to bugs. 5 minutes is a good starting point.

  4. Monitor canary events: Always listen to canary:promote, canary:rollback, and canary:complete events and log them to your monitoring system.

  5. Use circuit breakers on every route: Even if you trust your downstream services, circuit breakers prevent cascading failures during unexpected outages.

  6. Set per-route rate limits: Different services have different capacity. Set rate limits based on each service's throughput capability.

  7. Use traffic mirroring before canary: Before running a canary, mirror traffic to the new version to compare responses and catch obvious bugs.

  8. Keep the gateway stateless: The gateway should not store application state. All state (canary weights, circuit breaker state) is in-memory and rebuilds on restart from config.

What's Next?