Skip to main content
Version: 4.0.0-preview

Health & Monitoring

ExpressoTS v4 has health monitoring built in at three layers:

  1. A drop-in middleware health endpoint (Middleware.addHealthCheck()).
  2. A provider-level IHealthCheck interface that auto-discovers checks across your DI container.
  3. An aggregated health dashboard (registry.checkHealth()) that runs every check in parallel and reports an overall verdict.

You don't need a third-party library for any of this, but every layer is opt-in and composable.

A) Middleware health endpoint

The fastest way to expose GET /health from your application:

src/app.ts
import { AppExpress } from "@expressots/adapter-express";

export class App extends AppExpress {
async configureServices(): Promise<void> {
this.Middleware.applyPreset("api");

this.Middleware.addHealthCheck({
path: "/health", // default: "/health/middleware"
includeMetrics: true, // include profiler stats
detailed: true, // include per-middleware entries
});
}
}
$ curl -s localhost:3000/health | jq
{
"status": "healthy",
"timestamp": "2026-05-26T03:15:42.118Z",
"middleware": {
"total": 7,
"byCategory": { "parse": 2, "security": 2, "logger": 1, "compress": 1, "health": 1 },
"entries": [/* ... when detailed:true ... */]
},
"metrics": { /* ... when includeMetrics:true ... */ }
}

This endpoint reports the middleware pipeline's health: what's wired, in what order, and (with includeMetrics) how it's performing. It is intentionally cheap and synchronous.

For application-level health (databases, caches, downstream services), use the next layer.

B) IHealthCheck providers

Any provider can declare itself health-checkable by implementing IHealthCheck:

src/providers/cache.provider.ts
import {
provideSingleton,
IProvider,
IHealthCheck,
HealthCheckResult,
} from "@expressots/core";

@provideSingleton(CacheProvider)
export class CacheProvider implements IProvider, IHealthCheck {
readonly name = "cache";
readonly version = "1.0.0";

async healthCheck(): Promise<HealthCheckResult> {
const started = Date.now();
try {
await this.redis.ping();
return {
status: "healthy",
latency: Date.now() - started,
details: { connected: this.redis.status === "ready" },
};
} catch (error) {
return {
status: "unhealthy",
latency: Date.now() - started,
message: (error as Error).message,
};
}
}
}

HealthCheckResult shape:

interface HealthCheckResult {
status: "healthy" | "degraded" | "unhealthy";
latency?: number; // ms
message?: string; // human-readable
details?: Record<string, unknown>; // anything custom
checkedAt?: number; // populated by the registry
}

Three statuses keep the contract simple:

  • healthy: operating normally
  • degraded: running with reduced capacity (slow responses, partial outage, …)
  • unhealthy: broken or unreachable

The framework auto-discovers every provider that implements IHealthCheck. There is no central register-call.

C) The aggregated dashboard

ProviderRegistry.checkHealth() runs all IHealthCheck providers in parallel and returns:

interface HealthDashboard {
overall: "healthy" | "degraded" | "unhealthy"; // worst of all
providers: Array<{ name: string; result: HealthCheckResult }>;
checkedAt: number;
}

overall is the worst status across providers: one unhealthy provider makes the whole system unhealthy. This is the right default for orchestrators (k8s readiness probes, ALB target groups, etc.).

Expose it as your real /health endpoint:

src/app.controller.ts
import { provide, inject, ProviderRegistry } from "@expressots/core";
import { controller, Get } from "@expressots/adapter-express";

@provide(AppController)
@controller("/")
export class AppController {
constructor(
@inject(ProviderRegistry) private readonly registry: ProviderRegistry,
) {}

@Get("/health")
async health() {
return this.registry.checkHealth();
}
}
// 200 OK when everything is healthy, 503 if you map status → HTTP code
{
"overall": "healthy",
"providers": [
{ "name": "cache", "result": { "status": "healthy", "latency": 2 } },
{ "name": "database", "result": { "status": "healthy", "latency": 7 } }
],
"checkedAt": 1717999542118
}

For a Kubernetes-style split, expose two routes:

@Get("/livez")
livez() { return { status: "ok" }; }

@Get("/readyz")
async readyz(@response() res: Response) {
const dashboard = await this.registry.checkHealth();
const code = dashboard.overall === "healthy" ? 200 : 503;
return res.status(code).json(dashboard);
}

/livez is cheap and process-level. /readyz runs the full IHealthCheck matrix.

Logger health monitoring

The framework's logger has its own internal HealthMonitor that tracks throughput, dropped messages, queue saturation, and (optionally) sampled CPU usage:

import { Logger } from "@expressots/core";

const logger = new Logger({
health: {
enabled: true,
sampleIntervalMs: 1000,
cpuWarningThreshold: 80,
},
});

logger.startHealthMonitoring();
// ...
logger.stopHealthMonitoring(); // call this on shutdown

This is configured per logger instance, not via the middleware health check. They monitor different things. In a typical app you don't have to touch this; the defaults are sane and the monitor is automatically torn down on graceful shutdown.

Studio integration

Both signals (middleware pipeline health and IHealthCheck results) are surfaced in the Studio runtime panel. Each provider shows up with a health icon (💚 / 🟡 / 🔴) and the latest latency, so you can spot degraded dependencies without curling endpoints by hand.

For a typical API, this is the minimum useful stack:

src/app.ts
export class App extends AppExpress {
async configureServices() {
this.Middleware.applyPreset("api");
this.Middleware.addHealthCheck({ path: "/health/middleware" });
// /health and /readyz are exposed by your AppController (above)
}
}

Then implement IHealthCheck on each provider that talks to the outside world (database, cache, message broker, third-party APIs). The dashboard takes care of the rest.

See also

  • Bootstrap: Middleware.addHealthCheck configuration.
  • Providers: registering, scoping, and discovering providers.
  • Logging: the logger's own health monitor.