Gateway Package
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-Versionheader, 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:#fffKey Components
- GatewayServer: The main orchestrator — matches routes, applies policies, and forwards requests
- ServiceProxy: HTTP client that resolves service instances via discovery and applies resilience patterns
- CanaryEngine: Manages canary deployment lifecycle — traffic weights, metric evaluation, promotion steps, and rollback
- VersionRouter: Resolves target version from headers, URI paths, query params, or weighted distribution
- GatewayMetrics: Per-route and per-version metrics collection
- TrafficMirror: Sends shadow traffic to test services
- GatewayModule: Integration with
@hazeljs/configfor 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
| Prefix | Scope | Example |
|---|---|---|
GATEWAY_* | Global gateway settings | GATEWAY_DEFAULT_TIMEOUT=5000 |
GATEWAY_CB_* | Default circuit breaker | GATEWAY_CB_THRESHOLD=5 |
<SERVICE>_SVC_* | Per-service overrides | USER_SVC_RATE_LIMIT_MAX=100 |
<SERVICE>_CANARY_* | Canary deployment | ORDER_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:#fffPromotion Strategies
| Strategy | Metric | Use Case |
|---|---|---|
error-rate | Percentage of 5xx responses | Most common — catches bugs and crashes |
latency | p99 or average latency threshold | Catches performance regressions |
custom | Your own evaluation function | Complex 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
| Strategy | How It Works | Example |
|---|---|---|
| Header | Client sends X-API-Version: v2 | Opt-in for specific clients |
| URI | Path prefix /v2/api/users | RESTful versioning |
| Query | ?version=v2 | Quick testing |
| Weighted | Percentage-based random | A/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:
| Pattern | Matches | Example |
|---|---|---|
/api/users/** | Any path under /api/users/ | /api/users/123/orders |
/api/users/:id | Single path parameter | /api/users/123 |
/api/users/* | Single segment wildcard | /api/users/123 but not /api/users/123/orders |
/api/users | Exact match | Only /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
-
Use config-driven routes in production: Decorators are convenient for prototyping, but config-driven routes let you change behavior without redeploying.
-
Start with conservative canary weights: Begin with 5-10% canary traffic. You can always increase via env vars.
-
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.
-
Monitor canary events: Always listen to
canary:promote,canary:rollback, andcanary:completeevents and log them to your monitoring system. -
Use circuit breakers on every route: Even if you trust your downstream services, circuit breakers prevent cascading failures during unexpected outages.
-
Set per-route rate limits: Different services have different capacity. Set rate limits based on each service's throughput capability.
-
Use traffic mirroring before canary: Before running a canary, mirror traffic to the new version to compare responses and catch obvious bugs.
-
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?
- Learn about Resilience Package for circuit breaker, retry, and timeout patterns
- Explore Discovery Package for service registration and discovery
- Check out Config Package for environment-based configuration management