Health Check
Type-safe health monitoring with timeout protection, concurrent execution, and framework adapters for Express, Fastify, and Hono.
The Health Check block provides a standardized way to monitor your application's dependencies and internal state.
Instead of scattering health checks across your codebase, define all your checks in one place, validate them during startup, and expose a health endpoint that load balancers and monitoring systems can query.
Features
- Concurrent execution for fast health checks
- Timeout protection for each individual check
- Smart status mapping — healthy, degraded, or unhealthy
- Framework adapters for Express, Fastify, and Hono
- Type-safe configuration with full TypeScript support
- Fail-fast validation on startup
- Extension points for custom status logic and report building
- Sanitized errors to prevent leaking stack traces
File Structure
health-check
├── adapters
│ ├── express.ts
│ ├── fastify.ts
│ └── hono.ts
├── core
│ ├── build-report.ts
│ ├── calculate-status.ts
│ ├── create-health.ts
│ ├── run-check.ts
│ ├── run-checks.ts
│ └── validate-config.ts
├── types
│ └── index.ts
└── index.ts- adapters/ — Route registration functions for Express, Fastify, and Hono.
- core/ — Health check engine: factory, runner, status calculator, report builder, config validator.
- types/ — Shared TypeScript type definitions.
Installation
pnpm dlx blockend-cli add health-checkDetect Project
Blockend detects your project configuration and determines the correct output location.
Install Dependencies
Required packages are installed automatically.
Generate Files
The health check block is generated inside your configured blocks directory.
Copy the files below into your project's blocks directory.
Peer Dependencies
| Package | Required for |
|---|---|
express | Express adapter |
fastify | Fastify adapter |
hono | Hono adapter |
blocks/health-check/types/index.ts
Core type definitions for health checks, results, reports, and extension points.
// health status of the entire system
export type HealthStatus = "healthy" | "degraded" | "unhealthy";
// status of an individual check
export type CheckStatus = "healthy" | "unhealthy";
export interface HealthCheck {
/** Unique name of the check. Example: "database", "redis" */
name: string;
/** Whether failure should make the application unhealthy. */
critical: boolean;
/** Optional human-friendly message shown when the check fails. */
message?: string;
/** Timeout for this specific check in milliseconds. Prevents hanging. */
timeoutMs?: number;
/** Actual health check implementation. Throw an error if it fails. */
run(): Promise<void>;
}
export interface HealthCheckResult {
name: string;
critical: boolean;
status: CheckStatus;
duration: number; // in milliseconds
message?: string;
error?: string; // Sanitized to string to prevent leaking stack traces
}
export interface HealthReport {
status: HealthStatus;
timestamp: string;
uptime: number; // in seconds
checks: HealthCheckResult[];
}
// Extension points: Allow teams to override default policies
export type StatusCalculator = (results: HealthCheckResult[]) => HealthStatus;
export type ReportBuilder = (results: HealthCheckResult[], status: HealthStatus) => HealthReport;
export interface CreateHealthOptions {
checks: HealthCheck[];
/** Global fallback timeout in milliseconds (default: 5000) */
defaultTimeoutMs?: number;
/** Optional custom status calculation logic */
calculateStatus?: StatusCalculator;
/** Optional custom report generation logic */
buildReport?: ReportBuilder;
}
export interface Health {
run(): Promise<HealthReport>;
}
blocks/health-check/core/validate-config.ts
Fail-fast configuration validation.
import type { CreateHealthOptions } from "../types/index";
export function validateConfig(options: CreateHealthOptions): void {
if (!options.checks || !Array.isArray(options.checks)) {
throw new Error("Health configuration error: 'checks' must be a valid array.");
}
if (options.checks.length === 0) {
throw new Error("Health configuration error: 'checks' array cannot be empty.");
}
const names = new Set<string>();
for (const check of options.checks) {
if (!check.name || typeof check.name !== "string" || check.name.trim() === "") {
throw new Error(
"Health configuration error: Each check must have a non-empty string 'name'."
);
}
if (names.has(check.name)) {
throw new Error(
`Health configuration error: Duplicate check name found: "${check.name}". Names must be unique.`
);
}
names.add(check.name);
if (typeof check.run !== "function") {
throw new Error(
`Health configuration error: Check "${check.name}" is missing a valid 'run' function.`
);
}
if (
check.timeoutMs !== undefined &&
(typeof check.timeoutMs !== "number" || check.timeoutMs <= 0)
) {
throw new Error(
`Health configuration error: Check "${check.name}" has an invalid 'timeoutMs'. It must be a positive number.`
);
}
}
if (
options.defaultTimeoutMs !== undefined &&
(typeof options.defaultTimeoutMs !== "number" || options.defaultTimeoutMs <= 0)
) {
throw new Error("Health configuration error: 'defaultTimeoutMs' must be a positive number.");
}
}
blocks/health-check/core/run-check.ts
Runs a single check with timeout enforcement and error sanitization.
import type { HealthCheck, HealthCheckResult } from "../types/index";
export async function runCheck(
check: HealthCheck,
defaultTimeoutMs = 5000
): Promise<HealthCheckResult> {
const start = performance.now();
const timeout = check.timeoutMs ?? defaultTimeoutMs;
try {
// Enforce timeout to prevent a single slow dependency from hanging the entire health request
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => reject(new Error(`Check timed out after ${timeout}ms`)), timeout);
});
await Promise.race([check.run(), timeoutPromise]);
return {
name: check.name,
critical: check.critical,
status: "healthy",
duration: performance.now() - start,
...(check.message === undefined ? {} : { message: check.message })
};
} catch (error) {
const duration = performance.now() - start;
// Sanitize error: never expose raw Error objects (stack traces) in production APIs
const errorMessage = error instanceof Error ? error.message : String(error);
return {
name: check.name,
critical: check.critical,
status: "unhealthy",
duration,
...(check.message === undefined ? {} : { message: check.message }),
error: errorMessage
};
}
}
blocks/health-check/core/run-checks.ts
Runs all checks concurrently using Promise.all.
import type { HealthCheck, HealthCheckResult } from "../types/index";
import { runCheck } from "./run-check";
export async function runChecks(
checks: HealthCheck[],
defaultTimeoutMs?: number
): Promise<HealthCheckResult[]> {
const tasks = checks.map((check) => runCheck(check, defaultTimeoutMs));
return await Promise.all(tasks);
}
blocks/health-check/core/calculate-status.ts
Determines overall status from individual check results.
import type { HealthCheckResult, HealthStatus } from "../types/index";
export function calculateStatus(results: HealthCheckResult[]): HealthStatus {
const hasCriticalFailure = results.some(
(result) => result.critical && result.status === "unhealthy"
);
if (hasCriticalFailure) {
return "unhealthy";
}
const hasOptionalFailure = results.some(
(result) => !result.critical && result.status === "unhealthy"
);
if (hasOptionalFailure) {
return "degraded";
}
return "healthy";
}
blocks/health-check/core/build-report.ts
Assembles the final health report with timestamp and uptime.
import type { HealthCheckResult, HealthReport, HealthStatus } from "../types/index";
export function buildReport(results: HealthCheckResult[], status: HealthStatus): HealthReport {
// Safely get uptime for Node.js, fallback to 0 for other environments
const uptime =
typeof process !== "undefined" && typeof process.uptime === "function" ? process.uptime() : 0;
return {
status,
checks: results,
timestamp: new Date().toISOString(),
uptime
};
}
blocks/health-check/core/create-health.ts
Factory function that validates config and wires everything together.
import type { CreateHealthOptions, Health } from "../types/index";
import { buildReport as defaultBuildReport } from "./build-report";
import { calculateStatus as defaultCalculateStatus } from "./calculate-status";
import { runChecks } from "./run-checks";
import { validateConfig } from "./validate-config";
export function createHealth(options: CreateHealthOptions): Health {
// Fail fast: Validate configuration immediately upon creation
validateConfig(options);
// Allow teams to override default policies if business rules differ
const calculateStatus = options.calculateStatus ?? defaultCalculateStatus;
const buildReport = options.buildReport ?? defaultBuildReport;
return {
async run() {
const results = await runChecks(options.checks, options.defaultTimeoutMs);
const status = calculateStatus(results);
return buildReport(results, status);
}
};
}
blocks/health-check/adapters/express.ts
Express health route registration.
import type { Express, Request, Response } from "express";
import type { Health } from "../types/index"; // Adjust path to your core block
export function registerExpressHealthRoute(app: Express, health: Health, path = "/health") {
app.get(path, async (_req: Request, res: Response) => {
const report = await health.run();
// 503 for unhealthy (pull from load balancer)
// 200 for healthy/degraded (keep in rotation, let the JSON report show the degraded state)
const statusCode = report.status === "unhealthy" ? 503 : 200;
res.status(statusCode).json(report);
});
}
blocks/health-check/adapters/fastify.ts
Fastify health route registration.
import type { FastifyInstance } from "fastify";
import type { Health } from "../types/index";
export function registerFastifyHealthRoute(app: FastifyInstance, health: Health, path = "/health") {
app.get(path, async (_request, reply) => {
const report = await health.run();
const statusCode = report.status === "unhealthy" ? 503 : 200;
reply.code(statusCode).send(report);
});
}
blocks/health-check/adapters/hono.ts
Hono health route registration.
import type { Hono } from "hono";
import type { Health } from "../types/index";
export function registerHonoHealthRoute(app: Hono, health: Health, path = "/health") {
app.get(path, async (c) => {
const report = await health.run();
const statusCode = report.status === "unhealthy" ? 503 : 200;
return c.json(report, statusCode);
});
}
Configuration
CreateHealthOptions
| Option | Type | Default | Description |
|---|---|---|---|
checks | HealthCheck[] | Required | Array of health checks to run |
defaultTimeoutMs | number | 5000 | Global fallback timeout in ms |
calculateStatus | StatusCalculator | Default calculator | Custom status calculation logic |
buildReport | ReportBuilder | Default report builder | Custom report generation logic |
HealthCheck
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Unique identifier for the check |
critical | boolean | Yes | If true, failure makes the application unhealthy |
run | () => Promise<void> | Yes | Async function that throws on failure |
timeoutMs | number | No | Per-check timeout override |
message | string | No | Human-friendly message |
Architecture
createHealth(options)
│
▼
validateConfig(options) — fail fast on startup
│
▼
health.run()
│
▼
runChecks(checks) — concurrent execution via Promise.all
│
├── runCheck(check 1) ────► timeout → sanitize error → result
├── runCheck(check 2) ────► success → healthy result
└── runCheck(check N) ────► ...
│
▼
calculateStatus(results)
│ ├── All pass → "healthy"
│ ├── Critical fail → "unhealthy"
│ └── Optional fail → "degraded"
▼
buildReport(results, status)
│
▼
HealthReport (JSON)
│
▼
Adapter → HTTP Response (200 or 503)The factory validates configuration immediately. When health.run() is called, all checks execute concurrently with timeout protection. Results are collected, the overall status is computed, and a report is built. The adapter registers a route that returns the report with the appropriate HTTP status code.
When to Use
- You need a standardized health endpoint for load balancers and monitoring.
- You want timeout protection to prevent health checks from hanging.
- You need concurrent execution for fast health reports.
When Not to Use
- You prefer a lightweight, inline health check without configuration overhead.
- You don't need custom status calculation or report building.
Usage
Express
import express from "express";
import { createHealth } from "@/blocks/health-check";
import { registerExpressHealthRoute } from "@/blocks/health-check/adapters/express";
const health = createHealth({
checks: [
{
name: "database",
critical: true,
timeoutMs: 5000,
async run() {
await db.ping();
}
},
{
name: "redis",
critical: false,
timeoutMs: 3000,
async run() {
await redis.ping();
}
}
]
});
const app = express();
registerExpressHealthRoute(app, health); // GET /healthFastify
import Fastify from "fastify";
import { createHealth } from "@/blocks/health-check";
import { registerFastifyHealthRoute } from "@/blocks/health-check/adapters/fastify";
const health = createHealth({ checks: [...] });
const app = Fastify();
registerFastifyHealthRoute(app, health); // GET /healthHono
import { Hono } from "hono";
import { createHealth } from "@/blocks/health-check";
import { registerHonoHealthRoute } from "@/blocks/health-check/adapters/hono";
const health = createHealth({ checks: [...] });
const app = new Hono();
registerHonoHealthRoute(app, health); // GET /healthCustom Status Calculator
Override the default status logic:
const health = createHealth({
checks: [...],
calculateStatus: (results) => {
const allHealthy = results.every((r) => r.status === "healthy");
const anyFailed = results.some((r) => r.status === "unhealthy");
if (allHealthy) return "healthy";
if (anyFailed) return "unhealthy";
return "degraded";
},
});Custom Report Builder
Add custom fields to the health report:
const health = createHealth({
checks: [...],
buildReport: (results, status) => ({
status,
checks: results,
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: "v1.2.3",
}),
});API Reference
createHealth
function createHealth(options: CreateHealthOptions): Health;Creates a health check instance. Validates configuration immediately (fail fast).
| Parameter | Type | Required | Description |
|---|---|---|---|
options | CreateHealthOptions | Yes | Health check configuration |
Returns — Health object with a run() method.
Throws — On invalid configuration (empty checks, missing names, duplicates, missing run).
health.run
async function run(): Promise<HealthReport>;Executes all checks concurrently and returns a health report.
registerExpressHealthRoute
function registerExpressHealthRoute(app: Express, health: Health, path?: string): void;Registers a GET route at the specified path (default: /health). Returns 200 for healthy/degraded, 503 for unhealthy.
registerFastifyHealthRoute
function registerFastifyHealthRoute(app: FastifyInstance, health: Health, path?: string): void;registerHonoHealthRoute
function registerHonoHealthRoute(app: Hono, health: Health, path?: string): void;HealthReport
interface HealthReport {
status: "healthy" | "degraded" | "unhealthy";
timestamp: string; // ISO 8601
uptime: number; // seconds
checks: HealthCheckResult[];
}HealthCheckResult
interface HealthCheckResult {
name: string;
critical: boolean;
status: "healthy" | "unhealthy";
duration: number; // milliseconds
message?: string;
error?: string; // sanitized error message
}HealthCheck
interface HealthCheck {
name: string;
critical: boolean;
message?: string;
timeoutMs?: number;
run(): Promise<void>;
}Examples
Production Setup with Multiple Checks
A realistic production config with critical and non-critical checks:
const health = createHealth({
checks: [
{
name: "postgres",
critical: true,
async run() {
await db.raw("SELECT 1");
}
},
{
name: "redis",
critical: false,
async run() {
await redis.ping();
}
},
{
name: "external-api",
critical: false,
timeoutMs: 8000,
async run() {
const res = await fetch("https://api.example.com/health");
if (!res.ok) throw new Error(`API returned ${res.status}`);
}
}
]
});Testing
describe("Health Checks", () => {
it("returns healthy when all dependencies pass", async () => {
const health = createHealth({
checks: [{ name: "test", critical: true, run: async () => {} }]
});
const report = await health.run();
expect(report.status).toBe("healthy");
});
it("returns unhealthy when a critical check fails", async () => {
const health = createHealth({
checks: [
{
name: "failing",
critical: true,
run: async () => {
throw new Error("Failed");
}
}
]
});
const report = await health.run();
expect(report.status).toBe("unhealthy");
});
});Related Blocks
- Logger — Log health check results for monitoring and alerting.
FAQ
How are errors sanitized?
Error messages are extracted from Error objects using .message. Non-Error throws are converted to strings. Stack traces are never included in health check responses.
What happens when a check times out?
The check is rejected with a timeout error and reported as unhealthy. The rest of the checks continue executing.
What HTTP status codes do the adapters return?
200 for healthy and degraded statuses, 503 for unhealthy. This allows load balancers to keep a degraded node in rotation while pulling an unhealthy node.
Can I add custom fields to the health report?
Yes. Use the buildReport extension point to add any custom fields to the report object.
Environment Configuration
Type-safe environment variable validation with Zod. Fail fast at startup and access fully typed configuration throughout your application.
Graceful Shutdown
Coordinated, dependency-free shutdown with in-flight request draining, priority-ordered tasks, and adapters for Express, Fastify, and Hono.