Skip to content

Latest commit

 

History

History
183 lines (145 loc) · 6.12 KB

File metadata and controls

183 lines (145 loc) · 6.12 KB

node-healthz

The maintained, zero-dependency, TypeScript-first health check for Node.js, with a browser dashboard built in.

One endpoint returns JSON to your probes and monitors, and a human-friendly dashboard to your browser. The dashboard is self-contained (inline CSS, no JS, no CDN), so it renders offline, on an air-gapped box, and under a default-src 'self' CSP. Failure reasons show inline on each check row, with the full unmasked output in a hover bubble. It also tells degraded (healthy, but with a failing optional check) apart from all-green, while the JSON output and status code stay the same (healthy/200).

node-healthz HTML dashboard

Quickstart

npm install node-healthz
import express from 'express'
import * as healthz from 'node-healthz'

const app = express()

// Returns an Express-style handler; serves /healthz by default.
app.use(
  healthz.express({
    checks: [
      {
        id: 'PostgreSQL',
        required: true,
        fn: async () => {
          await pool.query('SELECT 1')
        },
      },
      {
        id: 'Redis', // not required: failure won't flip the report to NOT_OK
        fn: async () => {
          await redis.ping()
        },
      },
    ],
  }),
)

app.listen(3000)
# JSON for machines (probes, monitors): the default response
$ curl -si http://localhost:3000/healthz
HTTP/1.1 200 OK
{
  "status": "OK",
  "version": "2.1.0",
  "appName": "app",
  "checks": [
    { "id": "PostgreSQL", "status": "OK", "required": true,  "t": "1,003ms", "output": "<masked>" },
    { "id": "Redis",      "status": "OK", "required": false, "t": "11ms",    "output": "<masked>" }
  ]
}

# Same URL with `Accept: text/html` → the HTML dashboard
$ open http://localhost:3000/healthz

version is node-healthz's own library version (stamped automatically); appName defaults to "app". See Service identity to name the service, environment, and build.

A failing required check turns the report NOT_OK with HTTP 500; failing optional checks keep it OK / 200. Check output is masked by default so secrets in connection errors never leak through the health endpoint.

Service identity

When the same /healthz shape is served by many services, environments, and deploys, tell them apart with three optional flat fields:

app.use(
  healthz.express({
    appName: 'checkout-api', // optional, defaults to "app"
    envName: process.env.NODE_ENV, // optional, omitted when unset
    buildVer: process.env.GIT_SHA, // optional: a version, tag, or SHA; omitted when unset
    checks: [{ id: 'PostgreSQL', required: true, fn: () => pool.query('SELECT 1') }],
  }),
)

They surface in both outputs. In JSON, appName is always present and envName/buildVer appear only when set:

{
  "status": "OK",
  "version": "2.1.0",        // node-healthz's own version, automatic
  "appName": "checkout-api",
  "envName": "production",
  "buildVer": "8f3c1a2",
  "checks": [ /* ... */ ]
}

In the dashboard, the identity becomes an eyebrow above the status hero (CHECKOUT-API · PRODUCTION) and a footer that reads node-healthz · v2.1.0 on the left with build 8f3c1a2 on the right. There is no application-version field — the only version shown is node-healthz's own, as a provenance stamp. All identity values are HTML-escaped before rendering.

Why node-healthz

node-healthz @nestjs/terminus @godaddy/terminus lightship
Runtime dependencies 0 2 (+ 17 peer) 1 4
Last release 2026 2026 2023 2026
TypeScript-first typings only
HTML dashboard
Latency tiers
Masking by default
Framework integration any (or none) NestJS only bare http.Server own HTTP server

Verified against the npm registry, June 2026.

Recipes

Kubernetes readiness probe on its own path

app.use(
  healthz.express({
    path: '/readyz',
    checks: [{ id: 'db', required: true, fn: pingDb }],
  }),
)

Custom latency tiers + unmasked output

app.use(
  healthz.express({
    checks: [
      {
        id: 'Stripe',
        fn: checkStripe,
        maskOutput: false, // default is true (masking-by-default)
        latencyLevents: [50, 250], // default [100, 500] (note: key is misspelled in the public API)
      },
    ],
  }),
)

Framework-agnostic core (no Express): you own the transport

import { createServer } from 'node:http'
import * as healthz from 'node-healthz'

createServer(async (req, res) => {
  const result = await healthz.check({ checks })
  res.statusCode = healthz.status(result) // 200 healthy / 500 not-ok
  res.end(JSON.stringify(healthz.json(result))) // or healthz.html(result)
}).listen(3000)

Timeouts: global and per check

// Global timeout for every check (default 5000 ms) …
app.use(healthz.express({ timeout: 2000, checks }))

// … or per check, overriding the global
const result = await healthz.check({
  timeout: 1000,
  checks: [
    { id: 'db', required: true, fn: pingDb }, // 1s (global)
    { id: 'flakyVendor', fn: checkVendor, timeout: 8000 }, // 8s (own)
  ],
})

Precedence per check: check.timeout → top-level timeout5000. A check that loses the race reports TIMEOUT.

Guides