@fluojs/terminus
v2.0.3
Published
Health indicator composition and enriched runtime health aggregation for Fluo applications.
Maintainers
Readme
@fluojs/terminus
Health indicator toolkit for fluo applications. @fluojs/terminus layers on top of runtime health/readiness endpoints to provide dependency-aware status reporting.
The canonical contract is Health and Readiness. This README retains installation and essential package APIs and summarizes that Docs-owned contract; the sentinel above remains a checked summary. For startup, close order, and resource ownership, follow Lifecycle & Shutdown.
Node listener helpers now belong to @fluojs/platform-nodejs, not @fluojs/runtime/node. This package boundary change does not alter Terminus registration or readiness: TerminusModule.forRoot(...) remains application-owned, /health aggregates diagnostics, and /ready admits traffic only when readiness succeeds.
Table of Contents
- Installation
- When to Use
- Quick Start
- Common Patterns
- NestJS Migration Boundaries
- Public API Overview
- Related Packages
- Example Sources
Installation
pnpm add @fluojs/terminusInstall optional peers only for the indicator provider seams you enable:
pnpm add @fluojs/redis ioredis
pnpm add @fluojs/prisma @prisma/client
pnpm add @fluojs/drizzle drizzle-ormWhen to Use
- When you need to monitor external dependencies (databases, Redis, APIs) as part of your application's health status.
- When you want a structured JSON health report that aligns with standard monitoring patterns.
- When you need your
/readycheck to leave rotation while required downstream readiness signals are unavailable.
Quick Start
Import TerminusModule.forRoot() to register health indicators.
import { Module } from '@fluojs/core';
import { HttpHealthIndicator, TerminusModule } from '@fluojs/terminus';
import { MemoryHealthIndicator } from '@fluojs/terminus/node';
@Module({
imports: [
TerminusModule.forRoot({
indicators: [
HttpHealthIndicator.create({ key: 'upstream-api', url: 'https://example.com/health' }),
MemoryHealthIndicator.create({ key: 'memory', heapUsedThresholdRatio: 0.9 }),
],
}),
],
})
class AppModule {}Common Patterns
Built-in Indicators
The package provides several indicators out of the box:
PrismaHealthIndicator/DrizzleHealthIndicatorRedisHealthIndicator(from@fluojs/terminus/redis)HttpHealthIndicatorMemoryHealthIndicator/DiskHealthIndicator(from@fluojs/terminus/node)
When migrating from @nestjs/terminus, keep ownership boundaries explicit: create standalone indicators with XHealthIndicator.create(options) and register them in indicators. Use the DI provider factories only for Prisma, Drizzle, or Redis dependencies resolved through indicatorProviders. Import Node memory/disk helpers from @fluojs/terminus/node and Redis helpers from @fluojs/terminus/redis; Prisma, Drizzle, and HTTP indicators are root exports. The Redis and Node subpaths keep their dependency boundaries explicit.
DI-Backed Indicators
To use indicators that require dependencies from the DI container (like Redis or Database clients) without importing peer dependencies at module load time, use the documented provider factories. These helpers create indicator provider entries for TerminusModule.forRoot({ indicatorProviders }); they do not replace the module facade.
import { TerminusModule } from '@fluojs/terminus';
import { createRedisHealthIndicatorProvider } from '@fluojs/terminus/redis';
TerminusModule.forRoot({
indicatorProviders: [
createRedisHealthIndicatorProvider({ key: 'redis' })
],
});Omit clientName to keep probing the default Redis client. If the indicator should use a named Redis connection, pass clientName so the provider resolves that named client token instead of the default one.
TerminusModule.forRoot({
indicatorProviders: [
createRedisHealthIndicatorProvider({ key: 'redis' }),
createRedisHealthIndicatorProvider({ clientName: 'cache', key: 'cache-redis' }),
],
});Redis indicators created through @fluojs/terminus/redis are lifecycle-aware when they receive a client from @fluojs/redis. Terminus maps the Redis connection state with createRedisPlatformStatusSnapshot(...) before sending PING, so clients in wait, connecting, reconnecting, close, or end states mark /health and /ready unavailable instead of relying on a raw ping alone. Custom ping callbacks remain supported for manual probes, but lifecycle metadata is only attached when a Redis client exposes its status.
For Drizzle, createDrizzleHealthIndicatorProvider() prefers the lifecycle-aware DrizzleDatabase wrapper exported by @fluojs/drizzle. The indicator reports down before probing SQL whenever Drizzle is shutting down, stopped, or otherwise not ready according to DrizzleDatabase.createPlatformStatusSnapshot(). If only the legacy raw DRIZZLE_DATABASE handle is registered, the provider keeps the previous lightweight SQL probe behavior.
For Prisma, createPrismaHealthIndicatorProvider() prefers the lifecycle-aware PrismaService / PrismaServiceFacade token exported by @fluojs/prisma, checks createPlatformStatusSnapshot() before probing, and then calls current() so ambient transaction/lifecycle seams stay visible to the health probe. Omit name to target the default Prisma registration, pass name to target PrismaModule.forRoot({ name }), or pass explicit serviceToken / clientToken values for manual provider graphs. If only a raw Prisma client token is registered, the provider keeps the previous lightweight query probe behavior without importing the optional Prisma peer from the root package.
Provider factories are repeatable. You may register multiple providers created by the same factory in one indicatorProviders array when each instance uses a distinct indicator key or dependency option; Terminus keeps every provider instance under its own DI token instead of letting later same-type providers overwrite earlier ones.
Composing DI-Backed Indicators With Dependency Modules
Terminus resolves indicatorProviders inside its own module scope. Importing PrismaModule, DrizzleModule, or a named RedisModule registration into the surrounding application module does not make its exported tokens visible to Terminus, because ordinary module exports are only visible to the modules that import them. Pass the dependency-owning modules through imports so Terminus resolves them in the scope where the indicators live.
import { DrizzleModule } from '@fluojs/drizzle';
import { PrismaModule } from '@fluojs/prisma';
import { TerminusModule } from '@fluojs/terminus';
import { createDrizzleHealthIndicatorProvider, createPrismaHealthIndicatorProvider } from '@fluojs/terminus';
const prismaModule = PrismaModule.forRoot({ client });
const drizzleModule = DrizzleModule.forRoot({ database, dispose });
@Module({
imports: [
prismaModule,
drizzleModule,
TerminusModule.forRoot({
imports: [prismaModule, drizzleModule],
indicatorProviders: [
createPrismaHealthIndicatorProvider({ key: 'prisma' }),
createDrizzleHealthIndicatorProvider({ key: 'drizzle' }),
],
}),
],
})
class AppModule {}The same rule applies to named Redis registrations, which are always scoped and can never be reached through a global module:
const cacheRedisModule = RedisModule.forRoot({ host: '127.0.0.1', name: 'cache', port: 6379 });
TerminusModule.forRoot({
imports: [cacheRedisModule],
indicatorProviders: [
createRedisHealthIndicatorProvider({ clientName: 'cache', key: 'cache-redis' }),
],
});A named Redis indicator dependency is required. If no imported or global module supplies its token, module-graph validation fails during bootstrap with a MODULE_VISIBILITY_ERROR naming the missing token. Prisma and Drizzle indicator dependencies are optional only when their tokens are absent from the bootstrap graph: omitting their owner modules then allows the application to bootstrap and the corresponding indicator reports down in /health at request time. If a Prisma or Drizzle owner module exists elsewhere in the application but is omitted from Terminus imports, bootstrap instead fails with MODULE_VISIBILITY_ERROR; optional injection never bypasses module visibility. Dependencies published by a global module — for example RedisModule.forRoot(...) without a name, which is global by default — remain visible without an explicit imports entry.
Endpoint Middleware
Use class-based endpointMiddleware to protect only Terminus-generated endpoints. Classes resolve through DI and run in declaration order for both /health and /ready; omitting the option preserves the default unprotected behavior. Custom path values are normalized before the middleware route match.
import { ForbiddenException, type MiddlewareContext, type Next } from '@fluojs/http';
class HealthProbeAuthMiddleware {
async handle(context: MiddlewareContext, next: Next): Promise<void> {
if (context.request.headers['x-health-token'] !== 'secret-token') {
throw new ForbiddenException('Health endpoints require x-health-token.');
}
await next();
}
}
TerminusModule.forRoot({
endpointMiddleware: [HealthProbeAuthMiddleware],
path: '/internal/',
});The middleware above runs for normalized /internal/health and /internal/ready routes, not for unrelated application routes.
Readiness Participation
Every indicator participates in both /health and /ready by default. Set readiness: false when a dependency must remain visible in /health but must not remove an otherwise ready instance from rotation.
TerminusModule.forRoot({
indicators: [
HttpHealthIndicator.create({
key: 'search',
readiness: false,
url: 'https://search.example.com/health',
}),
MemoryHealthIndicator.create({ key: 'memory', heapUsedThresholdRatio: 0.9 }),
],
});If search reports down, /health returns 503 with its diagnostic while /ready continues to evaluate memory, custom readinessChecks, and platform readiness. A failing indicator whose readiness is omitted or true continues to make both endpoints unavailable.
Execution Guardrails
Use execution.indicatorTimeoutMs when custom indicators might hang or depend on slow downstreams. When a probe exceeds the configured timeout, Terminus marks that indicator as down instead of waiting forever.
Terminus also serializes checks per indicator instance inside each TerminusHealthService / application container. If a timed-out or otherwise slow probe is still running when another /health or /ready request arrives for the same container, Terminus reports that indicator as down for the new request instead of starting an overlapping probe against the same downstream. Separate application containers keep independent in-flight state even when tests or multi-app processes reuse the same indicator object. Built-in HTTP indicators abort their fetch request when their own timeout expires; other drivers and custom callbacks may not expose cancellation, so they are protected from overlap until the original promise settles.
TerminusModule.forRoot({
execution: {
indicatorTimeoutMs: 1_500,
},
indicators: [
HttpHealthIndicator.create({ key: 'upstream-api', url: 'https://example.com/health' }),
],
});Use path to mount the health endpoints under a custom path. Indicators gate /ready by default; set an indicator's readiness: false to retain its /health diagnostics without blocking traffic. readinessChecks composes additional application-specific conditions with indicator and platform readiness checks; it does not exclude indicators.
Failure Semantics
When an indicator returns a down result or throws a HealthCheckError, the TerminusHealthService aggregates the failure into a report:
/healthreturns the aggregatedHealthCheckReportdiagnostics and returns HTTP200only when that report is healthy; any indicator failure returns HTTP503./readymakes a binary traffic-admission decision: HTTP200admits traffic and HTTP503removes the instance from rotation. Its JSON body reports{ status: 'ready' },{ status: 'starting' }, or{ status: 'unavailable' }. An indicator whosereadinessis omitted ortruecan block the gate;readiness: falseretains its/healthdiagnostics without blocking/ready. CustomreadinessChecksonly add conditions. Platformcriticalmetadata stays in/healthdiagnostics, not readiness severity buckets.- The response body contains a structured JSON object with
status,contributors,info,error, anddetails. - Indicators may emit multiple keyed entries in a single check result;
/healthpreserves every keyed entry indetailsand in thecontributors.up/contributors.downsummaries. - Unsupported, empty, or non-object indicator results are reported as
downdiagnostics instead of being silently discarded. - Blank indicator result keys are reported as
downdiagnostics instead of contributing healthy entries. - If an indicator reuses a key that was already reported earlier in the same run, Terminus keeps the first entry and adds a deterministic
*-duplicate-key-errorcontributor instead of silently overwriting data. - Platform health/readiness failures are surfaced as deterministic
fluo-platform-healthandfluo-platform-readinesscontributors in/healthresponses. These keys are reserved for platform diagnostics; if a user indicator returns one of them during a platform failure, Terminus keeps the platform payload under the reserved key and adds a deterministic*-user-key-collisiondiagnostic instead of dropping runtime state. /healthresponses may include aplatformblock with platform health/readiness details when runtime diagnostics are available.- Prisma indicators created through the DI provider map
@fluojs/prismaservice lifecycle readiness/health state before querying, so shutdown, stopped, or not-yet-connected integrations mark/healthand/readyunavailable even when the raw client handle is still callable. - Drizzle indicators created through the DI provider map Drizzle lifecycle readiness/health state before SQL probing, so shutdown or stopped integrations mark
/healthand/readyas unavailable even if the underlying driver still accepts a raw ping. - Redis indicators created through the Redis subpath map
@fluojs/redisclient lifecycle state beforePING, so shutdown or disconnected Redis clients mark/healthand/readyas unavailable even before command execution.
NestJS Migration Boundaries
When migrating from @nestjs/terminus, treat TerminusModule.forRoot(...) as the primary fluo API. fluo does not model controller-level @HealthCheck() methods that call HealthCheckService.check([...]) as the main application contract. You can still call TerminusHealthService.check() directly from tests or custom application code, but production endpoint registration should keep indicators and readiness hooks in module options so the runtime /health and /ready routes include platform diagnostics consistently.
Terminus also does not create a separate process-only liveness route by default. The default route model remains GET /health for aggregated diagnostics and GET /ready for binary readiness with HTTP 200 or 503. If your deployment requires a narrow process liveness probe, define that probe at the application or deployment layer instead of assuming Terminus will add a NestJS-style extra route. These runtime-owned routes explicitly reject a NestJS controller's @HealthCheck() or @UseGuards() metadata; protect them with path-scoped application or adapter middleware, network policy, or a deployment-owned probe boundary.
Runtime-specific indicators are split by subpath. Use @fluojs/terminus/node for Node.js memory and disk checks, and use @fluojs/terminus/redis for Redis checks. Prisma and Drizzle provider helpers resolve token-only DI seams so the root package stays import-safe when those optional peers are absent, and Node disk filesystem access stays lazy so applications opt into runtime-specific probes explicitly.
Public API Overview
TerminusModule
static forRoot(options: TerminusModuleOptions): ModuleType- Main entry point for registering indicators and providers.
- Options include
imports,indicators,indicatorProviders,readinessChecks,execution.indicatorTimeoutMs, andpath.
TerminusHealthService
check(): Promise<HealthCheckReport>- Runs the currently registered indicators and returns the aggregated report.
isHealthy(): Promise<boolean>- Returns whether the current aggregated report is fully healthy.
isReady(): Promise<boolean>- Returns whether every indicator participating in readiness currently reports
up.
- Returns whether every indicator participating in readiness currently reports
HealthIndicator
readiness?: boolean- Controls whether an indicator participates in
/ready; it defaults totrue. All bundled indicator option objects accept this setting.
- Controls whether an indicator participates in
Direct helpers and tokens
runHealthCheck(...),assertHealthCheck(...): Direct aggregation/testing helpers.TERMINUS_HEALTH_INDICATORS,TERMINUS_INDICATOR_PROVIDER_TOKENS: DI tokens for registered indicators and provider tokens.TerminusModule.forRoot(...)exports both tokens so downstream modules can inspect the composed indicator/provider-token set without rebuilding Terminus internals.- Built-in indicators expose
XHealthIndicator.create()for standalone objects. Prisma, Drizzle, and Redis also expose DI provider helpers forindicatorProviders; application registration still goes throughTerminusModule.forRoot(...).
@fluojs/terminus/redis
RedisHealthIndicator.create(),createRedisHealthIndicatorProvider()- Redis-specific indicator helpers are exported from the dedicated subpath so the root package stays import-safe without the optional Redis peer installed.
- The provider resolves the default
@fluojs/redisclient token by default, or a named token whenclientNameis supplied, and reuses Redis platform status semantics for readiness diagnostics.
@fluojs/terminus/node
MemoryHealthIndicator.create(),DiskHealthIndicator.create()- Node-specific indicators are exported only from this dedicated subpath. Filesystem access for disk checks is lazy-loaded so importing the root package does not load Node filesystem modules at module initialization time.
HealthCheckError
- Throw this error within custom indicators to signal a "down" state.
Related Packages
@fluojs/metrics: Often used together for observability.@fluojs/prisma/@fluojs/drizzle/@fluojs/redis: Peer dependencies for specific indicators.
Example Sources
examples/ops-metrics-terminus/src/app.ts: End-to-end integration of health and metrics.packages/terminus/src/health-check.test.ts: Demonstrates aggregation and assertion flow.
Compatible service copies
TerminusHealthService supports compatible-copy class injection. Explicit indicator imports and providers remain required.
