NestJS course Β· Module 6: Error Handling and Monitoring
Health Checks - diagnosing the state
In this lesson6
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.
The package and its heart
In NestJS this is served by the @nestjs/terminus package. The name is unhelpful - there is no "health" in it - so it is easy to go looking for something like @nestjs/health or @nestjs/monitor. No such packages exist.
Its central element is HealthCheckService, 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.
The roll call
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: this.health.check([ opens the list, the individual indicators stand inside, and ]) 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.
The built-in indicators
Terminus provides several ready indicators, one per kind of resource:
TypeOrmHealthIndicator- checks the database connection withpingCheck.HttpHealthIndicator- queries an external HTTP address to see whether somebody else's API answers.MemoryHealthIndicator- watches memory usage;checkHeaptakes 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 shape of the response
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}status is one word for the whole - 'ok' when everything passed, or 'error' when anything failed. It is what a system asking about the application's health every dozen seconds reads.
info contains only the healthy indicators, while its twin field 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.
A custom indicator
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 HealthIndicator and implements the isHealthy(key) method. The contract has two sides and is worth remembering: on success you return this.getStatus(key, true), and on failure you throw HealthCheckError carrying the same status, only with 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: how many legions, how much free space, how long the check took.
Summary
The roll call is done and the cohort is fit for duty:
- a health check answers whether the application is fit to work - a running process does not guarantee that,
- the package for it is
@nestjs/terminus;@nestjs/health,@nestjs/diagnosticsand@nestjs/monitordo not exist, HealthCheckServiceruns a set of indicators and returns the aggregated status - it neither restarts the application nor sends alerts,- the order:
this.health.check([, the indicators,]), - you pass each indicator as a function
() => ...so the service can run it itself, TypeOrmHealthIndicatorchecks the database,HttpHealthIndicatoran external API,MemoryHealthIndicatormemory,DiskHealthIndicatorthe disk,- the response from general to specific:
status(one word),info(healthy only),details(all of them), - a custom indicator extends
HealthIndicatorand implementsisHealthy(key), - success:
return this.getStatus(key, true); failure:throw new HealthCheckError(...)- the exception distinguishes a failure from a broken check, - extra data from
getStatuslands indetailsand 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.
Code for this lesson: src/health/health-checks.ts
1// Health Checks - Diagnosing the State of the Imperium
2// Application health monitoring with @nestjs/terminus
3import { Injectable, Logger } from '@nestjs/common';
4import { Controller, Get } from '@nestjs/common';
5
6// ===========================================
7// 1. Health Indicator - health indicator
8// ===========================================
9
10@Injectable()
11export class DatabaseHealthIndicator {
12 private logger = new Logger('DBHealth');
13
14 async isHealthy(): Promise<{
15 status: 'up' | 'down';
16 details: Record<string, any>;
17 }> {
18 try {
19 // Simulating database connection check
20 const startTime = Date.now();
21 // await this.connection.query('SELECT 1');
22 const responseTime = Date.now() - startTime;
23
24 this.logger.log('Database: OK (' + responseTime + 'ms)');
25
26 return {
27 status: 'up',
28 details: {
29 responseTime: responseTime + 'ms',
30 connections: 5,
31 maxConnections: 20,
32 },
33 };
34 } catch (error) {
35 this.logger.error('Database: ERROR - ' + error.message);
36 return {
37 status: 'down',
38 details: { error: error.message },
39 };
40 }
41 }
42}
43
44@Injectable()
45export class MemoryHealthIndicator {
46 isHealthy() {
47 const used = process.memoryUsage();
48 const heapUsedMB = Math.round(used.heapUsed / 1024 / 1024);
49 const heapTotalMB = Math.round(used.heapTotal / 1024 / 1024);
50 const usagePercent = Math.round((heapUsedMB / heapTotalMB) * 100);
51
52 return {
53 status: usagePercent < 90 ? 'up' : 'down',
54 details: {
55 heapUsed: heapUsedMB + 'MB',
56 heapTotal: heapTotalMB + 'MB',
57 usage: usagePercent + '%',
58 },
59 };
60 }
61}
62
63// ===========================================
64// 2. Health Controller
65// ===========================================
66
67@Controller('health')
68export class HealthController {
69 constructor(
70 private dbHealth: DatabaseHealthIndicator,
71 private memoryHealth: MemoryHealthIndicator,
72 ) {}
73
74 @Get()
75 async check() {
76 const db = await this.dbHealth.isHealthy();
77 const memory = this.memoryHealth.isHealthy();
78
79 const overallStatus = db.status === 'up' && memory.status === 'up'
80 ? 'ok' : 'error';
81
82 return {
83 status: overallStatus,
84 info: {
85 database: db,
86 memory: memory,
87 },
88 timestamp: new Date().toISOString(),
89 };
90 }
91
92 @Get('liveness')
93 liveness() {
94 return { status: 'ok', uptime: process.uptime() };
95 }
96
97 @Get('readiness')
98 async readiness() {
99 const db = await this.dbHealth.isHealthy();
100 return {
101 status: db.status === 'up' ? 'ready' : 'not_ready',
102 database: db,
103 };
104 }
105}
106
107console.log('=== Health Checks ===');
108console.log('/health - full application status');
109console.log('/health/liveness - is the application alive');
110console.log('/health/readiness - is it ready for traffic');
111console.log('HealthIndicator - checks a specific component');
112Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which NestJS package is used to implement Health Checks?
2. What does HealthCheckService from @nestjs/terminus do?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Complete the HealthController with @Get('/health') that uses HealthCheckService.check() with TypeOrmHealthIndicator.pingCheck('database') and HttpHealthIndicator.pingCheck('api', 'http://localhost:4000')
- Click in order
Arrange the elements of the HealthCheckService.check() call in the correct order
- Code editor
Complete the LegionHealthIndicator class extending HealthIndicator with an isHealthy(key) method that returns this.getStatus(key, true) on success or throws a HealthCheckError on failure
- Vertical ordering
Arrange the Health Check response fields from overall status to details