Skip to main content

Node.js Integration

Consul Guardian runs as a sidecar next to your Node.js application. Your app reads configuration from Consul KV using the consul npm package or HTTP calls. Guardian independently watches those same keys, commits every change to Git, and lets you restore any key to a previous value.

No code changes are required in your Node.js application.

How Node.js apps typically read from Consul

PackageDescription
consulFull-featured Consul client for Node.js
node-fetch / built-in fetchDirect HTTP calls to Consul API

Example: consul npm package

npm install consul
const Consul = require("consul");

const consul = new Consul({ host: "consul", port: 8500 });

async function getConfig(key) {
const result = await consul.kv.get(key);
return result ? result.Value : null;
}

async function loadConfig() {
return {
dbHost: await getConfig("config/myapp/db_host"),
dbPort: parseInt(await getConfig("config/myapp/db_port"), 10),
cacheEnabled: (await getConfig("config/myapp/cache_enabled")) === "true",
maxRetries: parseInt(await getConfig("config/myapp/max_retries"), 10) || 3,
};
}

Example: Express app

const express = require("express");
const Consul = require("consul");

const app = express();
const consul = new Consul({ host: "consul", port: 8500 });

let config = {};

async function refreshConfig() {
const keys = [
"config/myapp/db_host",
"config/myapp/db_port",
"config/myapp/api_timeout",
];

for (const key of keys) {
const result = await consul.kv.get(key);
if (result) {
const shortKey = key.split("/").pop();
config[shortKey] = result.Value;
}
}
}

// Load config at startup, refresh every 30s
refreshConfig();
setInterval(refreshConfig, 30000);

app.get("/health", (req, res) => {
res.json({ status: "ok", config });
});

app.listen(3000);

Example: NestJS with Consul

// config.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import * as Consul from "consul";

@Injectable()
export class ConfigService implements OnModuleInit {
private consul: Consul.Consul;
private cache: Record<string, string> = {};

constructor() {
this.consul = new Consul({ host: "consul", port: 8500 });
}

async onModuleInit() {
await this.loadAll();
}

async loadAll() {
const keys = await this.consul.kv.keys("config/myapp/");
for (const key of keys) {
const result = await this.consul.kv.get(key);
if (result) {
this.cache[key] = result.Value;
}
}
}

get(key: string): string | undefined {
return this.cache[`config/myapp/${key}`];
}
}

Adding Guardian

Docker Compose

version: "3.8"

services:
consul:
image: hashicorp/consul:1.17
ports:
- "8500:8500"
command: agent -dev -client=0.0.0.0

myapp:
build: .
ports:
- "3000:3000"
environment:
- CONSUL_HOST=consul
- CONSUL_PORT=8500
depends_on:
- consul

guardian:
image: ghcr.io/consul-guardian/consul-guardian:latest
environment:
- CONSUL_GUARDIAN_CONSUL_ADDRESS=http://consul:8500
- CONSUL_GUARDIAN_WATCH_PREFIXES=config/,feature-flags/
- CONSUL_GUARDIAN_GIT_REPO_PATH=/data/repo
volumes:
- guardian-data:/data
depends_on:
- consul

volumes:
guardian-data:

PM2 + Consul scenario

If you run Node.js with PM2 in production and read config from Consul, Guardian fits into the same infrastructure:

version: "3.8"

services:
consul:
image: hashicorp/consul:1.17
command: agent -dev -client=0.0.0.0

myapp:
build: .
command: pm2-runtime ecosystem.config.js
environment:
- CONSUL_HOST=consul
depends_on:
- consul

guardian:
image: ghcr.io/consul-guardian/consul-guardian:latest
environment:
- CONSUL_GUARDIAN_CONSUL_ADDRESS=http://consul:8500
- CONSUL_GUARDIAN_WATCH_PREFIXES=config/,feature-flags/
- CONSUL_GUARDIAN_GIT_REPO_PATH=/data/repo
volumes:
- guardian-data:/data
depends_on:
- consul

volumes:
guardian-data:

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 3
selector:
matchLabels:
app: myapp
template:
metadata:
labels:
app: myapp
spec:
containers:
- name: myapp
image: myregistry/myapp:latest
ports:
- containerPort: 3000
env:
- name: CONSUL_HOST
value: "consul.consul.svc.cluster.local"
- name: CONSUL_PORT
value: "8500"

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: consul-guardian
spec:
replicas: 1
selector:
matchLabels:
app: consul-guardian
template:
metadata:
labels:
app: consul-guardian
spec:
containers:
- name: guardian
image: ghcr.io/consul-guardian/consul-guardian:latest
env:
- name: CONSUL_GUARDIAN_CONSUL_ADDRESS
value: "http://consul.consul.svc.cluster.local:8500"
- name: CONSUL_GUARDIAN_WATCH_PREFIXES
value: "config/,feature-flags/"
- name: CONSUL_GUARDIAN_GIT_REPO_PATH
value: "/data/repo"
volumeMounts:
- name: guardian-data
mountPath: /data
volumes:
- name: guardian-data
persistentVolumeClaim:
claimName: guardian-pvc

Restore scenario

A deploy script accidentally overwrites config/myapp/api_timeout from "5000" to "5" (5ms instead of 5 seconds). Every outbound API call starts timing out.

1. Find what happened

consul-guardian log --key config/myapp/api_timeout

# 2024-03-15T13:22:00Z bad1234 config/myapp/api_timeout modified
# 2024-03-01T10:00:00Z good567 config/myapp/api_timeout modified

2. Restore

consul-guardian restore --key config/myapp/api_timeout --commit good567

3. Verify

consul kv get config/myapp/api_timeout
# 5000 ← correct value restored

If your app refreshes config on an interval (like the Express example above), it picks up the fix automatically. If it reads config only at startup, restart the process or send a signal.

Zero code changes

Guardian does not add a dependency to your package.json, does not require middleware in Express or NestJS, and does not proxy your Consul reads. It is a separate process that watches Consul KV independently.