The application started, the process runs, the port answers. But is it truly fit for duty? The database may have refused a connection, the disk may be full, an external API may be silent. The process lives, and every request will fail anyway.
A Roman camp had a morning roll call for this. Nobody counted how many legionaries were breathing - they checked in turn: does the well give water, is the store not empty, did the messenger from the neighbouring fort arrive. Only the sum of those answers said whether the cohort could march. That is a health check.
In NestJS this is served by the
package. The name is unhelpful - there is no "health" in it - so it is easy to go looking for something like @nestjs/terminus
@nestjs/health or @nestjs/monitor. No such packages exist.Its central element is
, which runs a set of indicators and returns the application's aggregated status. It measures nothing itself and - importantly - neither restarts the application nor sends alerts. It gathers answers and reports the result; what to do with it is for whoever asked.HealthCheckService
A health controller looks like this:
1@Controller('health')
2export class HealthController {
3 constructor(
4 private health: HealthCheckService,
5 private db: TypeOrmHealthIndicator,
6 private memory: MemoryHealthIndicator,
7 ) {}
8
9 @Get()
10 @HealthCheck()
11 check() {
12 return this.health.check([
13 () => this.db.pingCheck('database'),
14 () => this.memory.checkHeap('memory', 150 * 1024 * 1024),
15 ]);
16 }
17}The call's order is always the same:
opens the list, the individual indicators stand inside, and this.health.check([
closes it.])
Note that you pass each indicator as a function, not as the result of calling it. Writing
() => this.db.pingCheck('database') lets the service run the checks itself - in parallel and with error handling. Had you written this.db.pingCheck('database') without the arrow, the check would run immediately, outside the service's control, and a thrown exception would take the whole endpoint down.Each indicator's first argument is a key - the name under which its result appears in the response. You choose it.
Terminus provides several ready indicators, one per kind of resource:
TypeOrmHealthIndicator - checks the database connection with pingCheck.HttpHealthIndicator - queries an external HTTP address to see whether somebody else's API answers.MemoryHealthIndicator - watches memory usage; checkHeap takes a threshold in bytes.DiskHealthIndicator - checks free disk space.The names are similar enough that it is worth remembering them by the resource they concern: TypeOrm - database, Http - external API, Memory - memory, Disk - disk.
The roll call's result has three levels of detail, from general to specific:
1{
2 "status": "ok",
3 "info": { "database": { "status": "up" } },
4 "details": { "database": { "status": "up" }, "memory": { "status": "up" } }
5}
is one word for the whole - status
'ok' when everything passed, or 'error' when anything failed. It is what a system asking about the application's health every dozen seconds reads.
contains only the healthy indicators, while its twin field info
error holds only the failing ones. details is the complete set: every indicator regardless of outcome.The split has a practical point: a human looks into
details for the full picture, while an automated system reads status, because that single value is what decides its action.The built-in indicators watch infrastructure. When you want to check something from your own domain - whether the treasury accepts deposits, whether a legion is at full strength - you write an indicator yourself:
1@Injectable()
2export class LegionHealthIndicator extends HealthIndicator {
3 constructor(private legionsService: LegionsService) {
4 super();
5 }
6
7 async isHealthy(key: string): Promise<HealthIndicatorResult> {
8 const count = await this.legionsService.countActive();
9
10 if (count > 0) {
11 return this.getStatus(key, true, { activeLegions: count });
12 }
13
14 throw new HealthCheckError(
15 'No active legions',
16 this.getStatus(key, false, { activeLegions: 0 }),
17 );
18 }
19}The class extends
and implements the HealthIndicator
method. The contract has two sides and is worth remembering: on success you return isHealthy(key)
, and on failure you throw this.getStatus(key, true)
carrying the same status, only with HealthCheckError
false.Why an exception rather than returning
false? Because HealthCheckService runs the indicators in parallel and must tell "I checked, it is bad" apart from "the check itself blew up". A thrown HealthCheckError carries both: the failure signal and a ready status to place in the response.The third argument of
getStatus is arbitrary extra data - it lands in details beside the status. That is a good place for numbers that will help a diagnosis, @name: how many legions, how much free space, how long the check took.The roll call is done and the cohort is fit for duty:
@nestjs/terminus; @nestjs/health, @nestjs/diagnostics and @nestjs/monitor do not exist,HealthCheckService runs a set of indicators and returns the aggregated status - it neither restarts the application nor sends alerts,this.health.check([, the indicators, ]),() => ... so the service can run it itself,TypeOrmHealthIndicator checks the database, HttpHealthIndicator an external API, MemoryHealthIndicator memory, DiskHealthIndicator the disk,status (one word), info (healthy only), details (all of them),HealthIndicator and implements isHealthy(key),return this.getStatus(key, true); failure: throw new HealthCheckError(...) - the exception distinguishes a failure from a broken check,getStatus lands in details and helps with diagnosis.In the next lesson we will move from a single roll call to continuous observation - you will meet metrics and early warning systems. For now remember: a living process is not the same as an application fit for duty - and only the roll call settles that.