A practical, in-depth exploration of the HazelJS API Gateway — from routing and versioning to canary deployments and production-ready patterns.
Table of Contents
- Introduction
- Why an API Gateway?
- Architecture Overview
- Core Concepts
- Getting Started
- Version Routing
- Canary Deployments
- Resilience Patterns
- Traffic Management
- Production Integration
- Best Practices
Introduction
The @hazeljs/gateway package is an intelligent API gateway designed for HazelJS microservices. It sits between your clients and backend services, providing:
- Unified entry point — Clients talk to one URL; the gateway routes to the right service
- Version routing — Route by header, URI, query param, or weighted distribution
- Canary deployments — Gradually shift traffic with automatic promotion or rollback
- Resilience — Circuit breaker, rate limiting, retries, and timeouts per route
- Traffic mirroring — Shadow traffic to test new versions without affecting users
- Real-time metrics — Per-route and per-version performance tracking
This guide walks you through each feature with practical examples and diagrams.
Why an API Gateway?
In a microservices architecture, clients would otherwise need to know about every service:
Without Gateway:
┌─────────┐ ┌─────────────────┐
│ Client │────▶│ user-service │ :3001
└─────────┘ └─────────────────┘
│
│ ┌─────────────────┐
├───────▶│ order-service │ :3002
│ └─────────────────┘
│
│ ┌─────────────────┐
└───────▶│ payment-service │ :3003
└─────────────────┘
Problems: Multiple URLs, CORS, auth per service, no central control
With a gateway, clients have a single entry point:
With Gateway:
┌─────────┐ ┌──────────────────────────────────────────┐
│ Client │────▶│ API Gateway │
└─────────┘ │ /api/users → user-service │
│ /api/orders → order-service │
│ /api/payments→ payment-service │
└──────────────────────────────────────────┘
│
┌───────────────┼───────────────┐
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ user-svc │ │ order-svc │ │ payment-svc │
└─────────────┘ └─────────────┘ └─────────────┘
Benefits: Single URL, central auth, routing, resilience, metrics
Architecture Overview
High-Level Flow
┌─────────────────┐
│ HTTP Client │
└────────┬────────┘
│ GET /api/users
▼
┌────────────────────────────────────────────────────────────────────────────┐
│ GATEWAY LAYER │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────┐ │
│ │Route Matcher │──▶│Version Router│──▶│Canary Engine │──▶│ServiceProxy │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────┬──────┘ │
│ │ │
│ ┌──────────────┐ ┌──────────────┐ │ │
│ │CircuitBreaker│◀──│ Rate Limiter │◀────────────────────────────┘ │
│ └──────────────┘ └──────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────┐
│ DiscoveryClient │
└────────┬────────┘
│ Load balanced
┌──────────────┼──────────────┐
▼ ▼ ▼
┌───────────┐ ┌───────────┐ ┌───────────┐
│user-svc v1│ │user-svc v2│ │order-svc │
└───────────┘ └───────────┘ └───────────┘
Request Lifecycle
Request: Client ──▶ Gateway ──▶ Route Match ──▶ Version Select ──▶ Canary Engine
│
▼
Service Proxy (circuit breaker + rate limit) ──▶ Discovery ──▶ Backend
Response: Client ◀── Gateway ◀── Service Proxy ◀── Backend
Key Components
| Component | Responsibility |
|---|---|
| GatewayServer | Main orchestrator; matches routes, applies policies, forwards requests |
| Route Matcher | Glob patterns, path params (:id), wildcards (*, **) |
| Version Router | Resolves target version from header, URI, query, or weighted random |
| Canary Engine | Manages canary lifecycle — traffic weights, metrics, promotion, rollback |
| Service Proxy | HTTP client that resolves instances via discovery, applies resilience |
| Circuit Breaker | Per-route protection; opens when failure threshold exceeded |
| Rate Limiter | Token bucket or sliding window; returns 429 when exceeded |
| Gateway Metrics | Per-route and per-version success/failure/latency tracking |
Core Concepts
Route Matching
The gateway supports flexible path patterns:
| Pattern | Matches | Example |
|---|---|---|
/api/users |
Exact | Only /api/users
|
/api/users/:id |
Path parameter |
/api/users/123 → { id: '123' }
|
/api/users/* |
Single segment |
/api/users/123 but not /api/users/123/orders
|
/api/users/** |
Multi-segment | /api/users/123/orders/456 |
Routes are sorted by specificity — more specific patterns match first:
1. /api/users/:id/orders (most specific)
2. /api/users/:id
3. /api/users/*
4. /api/users
5. /api/**
6. /** (catch-all)
Path Rewriting
The gateway can transform paths before forwarding:
{
path: '/api/users/**',
serviceName: 'user-service',
serviceConfig: {
stripPrefix: '/api/users', // Remove from incoming path
addPrefix: '/users', // Add to forwarded path
},
}
Example: GET /api/users/123 → forwarded as GET /users/123 to user-service.
Service Discovery Integration
The gateway uses @hazeljs/discovery to find backend instances. It supports:
- Memory backend — Development (in-memory)
- Redis backend — Production (shared state)
- Consul backend — Service mesh
- Kubernetes backend — K8s Endpoints API
Load balancing strategies: round-robin, random, least-connections, weighted-round-robin, IP hash, zone-aware.
Getting Started
1. Install Dependencies
npm install @hazeljs/gateway @hazeljs/discovery @hazeljs/resilience
2. Minimal Config-Driven Gateway
import { GatewayServer } from '@hazeljs/gateway';
import { MemoryRegistryBackend } from '@hazeljs/discovery';
const backend = new MemoryRegistryBackend();
// Register your services with the backend (see discovery docs)
// await registerUserService(backend, 3001);
const gateway = GatewayServer.fromConfig(
{
routes: [
{
path: '/api/users/**',
serviceName: 'user-service',
serviceConfig: {
stripPrefix: '/api/users',
addPrefix: '/users',
},
},
],
},
backend
);
// Handle a request
const response = await gateway.handleRequest({
method: 'GET',
path: '/api/users',
headers: { 'content-type': 'application/json' },
});
3. Run the Demos
From the hazeljs-gateway-starter directory:
# Start mock backend services
npm run start:services
# Config-driven gateway
npm run demo:gateway:config
# Gateway with HazelApp (production-style)
npm run demo:gateway:hazelapp
# Full-stack integration
npm run demo:scenario:full-stack
Version Routing
Version routing lets you serve multiple API versions from the same gateway path.
Strategy: Header
Clients opt-in by sending X-API-Version: v2:
{
path: '/api/users/**',
serviceName: 'user-service',
versionRoute: {
strategy: 'header',
header: 'X-API-Version',
defaultVersion: 'v1',
routes: {
v1: { weight: 80, allowExplicit: true, filter: { tags: ['v1'] } },
v2: { weight: 20, allowExplicit: true, filter: { tags: ['v2'] } },
},
},
}
Client A (no header) ──────────▶ Gateway ──▶ user-service v1 (80% traffic)
Client B (X-API-Version: v2) ──▶ Gateway ──▶ user-service v2 (20% traffic)
Strategy: URI Prefix
RESTful versioning: /v1/api/users, /v2/api/users
Strategy: Query Parameter
Quick testing: ?version=v2
Strategy: Weighted
A/B testing: 80% to v1, 20% to v2 (random distribution)
Canary Deployments
Canary deployments gradually shift traffic from a stable version to a new version. The gateway monitors error rates and latency, then automatically promotes or rolls back.
How It Works
Deploy v2 ──▶ 10% canary ──▶ Monitor 5 min
│
┌───────────────┴───────────────┐
│ │
errors < 5% errors > 5%
│ │
▼ ▼
25% ──▶ 50% ──▶ 75% ──▶ 100% ✓ Rollback 0% ✗
(Complete) (Abort)
Configuration
{
path: '/api/orders/**',
serviceName: 'order-service',
canary: {
stable: {
version: '1.0.0',
weight: 90,
filter: { metadata: { version: '1.0.0' } },
},
canary: {
version: '2.0.0',
weight: 10,
filter: { metadata: { version: '2.0.0' } },
},
promotion: {
strategy: 'error-rate',
errorThreshold: 5, // Rollback if errors > 5%
evaluationWindow: '5m', // Evaluate over 5 minutes
autoPromote: true,
autoRollback: true,
steps: [10, 25, 50, 75, 100],
stepInterval: '10m',
minRequests: 10, // Wait for enough data
},
},
}
Promotion Strategies
| Strategy | Metric | Use Case |
|---|---|---|
error-rate |
% of 5xx responses | Most common — catches bugs |
latency |
p99 or average threshold | Performance regressions |
custom |
Your evaluation function | Multi-metric decisions |
Events
gateway.on('canary:promote', (data) => {
console.log(`Step ${data.step}: canary at ${data.canaryWeight}%`);
});
gateway.on('canary:rollback', (data) => {
console.log(`Rolled back: ${data.canaryVersion} (trigger: ${data.trigger})`);
});
gateway.on('canary:complete', (data) => {
console.log(`${data.version} is now at 100%`);
});
Manual Control
const engine = gateway.getCanaryEngine('/api/orders/**');
engine.pause(); // Freeze canary progression
engine.resume(); // Resume
engine.promote(); // Force next step
engine.rollback(); // Force rollback to 0%
Resilience Patterns
The gateway uses @hazeljs/resilience for per-route protection.
Circuit Breaker
Prevents cascading failures by opening the circuit when too many requests fail:
CLOSED (Normal)
│
│ failures >= threshold
▼
OPEN (Blocking)
│
│ reset timeout
▼
HALF_OPEN (Testing)
│
┌─────┴─────┐
│ │
success failure
│ │
▼ ▼
CLOSED OPEN
{
path: '/api/payments/**',
serviceName: 'payment-service',
circuitBreaker: {
failureThreshold: 3,
successThreshold: 2,
resetTimeout: 15_000,
slidingWindow: { type: 'time', size: 60_000 },
},
}
Rate Limiting
Returns 429 Too Many Requests with Retry-After header when exceeded:
{
path: '/api/orders/**',
rateLimit: {
strategy: 'sliding-window',
max: 100,
window: 60_000, // 100 requests per minute
},
}
Strategies: token-bucket (allows bursts) or sliding-window (rolling limit).
Retry & Timeout
trafficPolicy: {
timeout: 8_000,
retry: {
maxAttempts: 3,
backoff: 'exponential',
baseDelay: 1_000,
maxDelay: 10_000,
jitter: true,
},
}
Traffic Management
Traffic Mirroring
Send a percentage of traffic to a shadow service without affecting the response:
{
path: '/api/products/**',
serviceName: 'product-service',
trafficPolicy: {
mirror: {
service: 'analytics-service',
percentage: 30,
waitForResponse: false, // Fire-and-forget
},
timeout: 5_000,
},
}
Use cases: Testing new versions with real traffic, comparing responses, performance benchmarking.
Request/Response Transforms
Modify headers or body in transit (see gateway docs for transform API).
Production Integration
Gateway with HazelApp
The recommended production setup: run the gateway behind HazelApp's HTTP server. This gives you:
- Health checks (
/health,/ready,/live) - CORS, body parsing, request ID
- Graceful shutdown
- Single process for gateway + custom controllers
import { HazelApp, HazelModule } from '@hazeljs/core';
import { GatewayServer, createGatewayHandler } from '@hazeljs/gateway';
const gateway = GatewayServer.fromConfig(config, backend);
gateway.startCanaries();
const app = new HazelApp(AppModule);
app.addProxyHandler('/api', createGatewayHandler(gateway));
await app.listen(3000);
Flow: Requests to /api/* go to the gateway; other paths (e.g. /health) go to HazelApp controllers.
Config from Environment Variables
For 12-factor apps, drive config from env vars:
const gatewayConfig = () => ({
gateway: {
routes: [
{
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: {
errorThreshold: parseInt(process.env.ORDER_CANARY_ERROR_THRESHOLD || '5'),
evaluationWindow: process.env.ORDER_CANARY_WINDOW || '5m',
},
},
},
],
},
});
Best Practices
1. Use Config-Driven Routes in Production
Decorators are great for prototyping; config-driven routes let you change behavior without redeploying.
2. Start with Conservative Canary Weights
Begin with 5–10% canary traffic. Increase via env vars as confidence grows.
3. Set Appropriate Evaluation Windows
- Too short: Not enough data, noisy decisions
- Too long: Users exposed to bugs longer
- 5 minutes is a good default
4. Monitor Canary Events
Always log canary:promote, canary:rollback, and canary:complete to your monitoring system.
5. Circuit Breakers on Every Route
Even if you trust downstream services, circuit breakers prevent cascading failures during outages.
6. Per-Route Rate Limits
Different services have different capacity. Set limits based on each service's throughput.
7. 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
All state (canary weights, circuit breaker state) is in-memory. On restart, the gateway rebuilds from config.
Summary
| Feature | Use Case |
|---|---|
| Version routing | Multiple API versions, A/B testing |
| Canary deployments | Safe rollouts with automatic rollback |
| Circuit breaker | Prevent cascading failures |
| Rate limiting | Protect downstream services |
| Traffic mirroring | Test with real traffic |
| Path rewriting | Clean API surface for clients |
| Metrics | Observability and canary decisions |
The @hazeljs/gateway package integrates seamlessly with @hazeljs/discovery and @hazeljs/resilience to provide a production-ready API gateway for your HazelJS microservices.
Further Reading
- Gateway Package Documentation
- Discovery Package
- Resilience Package
- hazeljs-gateway-starter Demos — Run the examples in this repo
Top comments (0)