NestJS course Β· Module 6: Error Handling and Monitoring

Health Checks - diagnosing the state

5 min read
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 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 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/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,
  • the order: this.health.check([, the indicators, ]),
  • you pass each indicator as a function () => ... so the service can run it itself,
  • TypeOrmHealthIndicator checks the database, HttpHealthIndicator an external API, MemoryHealthIndicator memory, DiskHealthIndicator the disk,
  • the response from general to specific: status (one word), info (healthy only), details (all of them),
  • a custom indicator extends HealthIndicator and implements isHealthy(key),
  • success: return this.getStatus(key, true); failure: throw new HealthCheckError(...) - the exception distinguishes a failure from a broken check,
  • extra data from 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.

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');
112

Spotted 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. 1. Which NestJS package is used to implement Health Checks?

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

Useful articles