Like Blockend? Give it a ⭐ on GitHub.

Star
blockend

Blockend

02 blocks

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-check

Detect 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

PackageRequired for
expressExpress adapter
fastifyFastify adapter
honoHono 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

OptionTypeDefaultDescription
checksHealthCheck[]RequiredArray of health checks to run
defaultTimeoutMsnumber5000Global fallback timeout in ms
calculateStatusStatusCalculatorDefault calculatorCustom status calculation logic
buildReportReportBuilderDefault report builderCustom report generation logic

HealthCheck

NameTypeRequiredDescription
namestringYesUnique identifier for the check
criticalbooleanYesIf true, failure makes the application unhealthy
run() => Promise<void>YesAsync function that throws on failure
timeoutMsnumberNoPer-check timeout override
messagestringNoHuman-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 /health

Fastify

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 /health

Hono

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 /health

Custom 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).

ParameterTypeRequiredDescription
optionsCreateHealthOptionsYesHealth check configuration

ReturnsHealth 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");
  });
});

  • 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.

On this page