Health Checks with Terminus

Liveness and Readiness

Load balancers, Docker and Kubernetes call a health endpoint to decide whether to send traffic to an instance or restart it.

Example

npm i @nestjs/terminus

Example output

added 2 packages in 2s

src/health/health.controller.ts

import { Controller, Get } from '@nestjs/common';
import { HealthCheck, HealthCheckService, MemoryHealthIndicator, PrismaHealthIndicator } from '@nestjs/terminus';
import { Public } from '../auth/public.decorator';
import { PrismaService } from '../prisma/prisma.service';

@Public()
@Controller('health')
export class HealthController {
  constructor(
    private readonly health: HealthCheckService,
    private readonly prismaHealth: PrismaHealthIndicator,
    private readonly memory: MemoryHealthIndicator,
    private readonly prisma: PrismaService,
  ) {}

  @Get()
  @HealthCheck()
  check() {
    return this.health.check([
      () => this.prismaHealth.pingCheck('database', this.prisma),
      () => this.memory.checkHeap('memory_heap', 400 * 1024 * 1024),
    ]);
  }
}

// health.module.ts: imports: [TerminusModule], controllers: [HealthController]

Example

curl -s localhost:3000/api/health
# database stopped:
curl -si localhost:3000/api/health

Example output

{"status":"ok","info":{"database":{"status":"up"},"memory_heap":{"status":"up"}},"error":{},"details":{"database":{"status":"up"},"memory_heap":{"status":"up"}}}

HTTP/1.1 503 Service Unavailable
{"status":"error","info":{"memory_heap":{"status":"up"}},"error":{"database":{"status":"down","message":"Can't reach database server at db:5432"}},"details":{...}}

How the Result is Combined

Example

Terminus runs every indicator, collects up/down results and answers 200 only if all are up:

type Indicator = () => Promise<Record<string, { status: 'up' | 'down'; message?: string }>>;

async function check(indicators: Indicator[]) {
  const results = await Promise.all(indicators.map((i) => i().catch((e: Error) => ({ unknown: { status: 'down' as const, message: e.message } }))));
  const details = Object.assign({}, ...results) as Record<string, { status: string; message?: string }>;
  const info = Object.fromEntries(Object.entries(details).filter(([, v]) => v.status === 'up'));
  const error = Object.fromEntries(Object.entries(details).filter(([, v]) => v.status !== 'up'));
  const ok = Object.keys(error).length === 0;
  return { httpStatus: ok ? 200 : 503, body: { status: ok ? 'ok' : 'error', info, error } };
}

const db = (up: boolean): Indicator => async () =>
  up ? { database: { status: 'up' } } : { database: { status: 'down', message: "Can't reach database server" } };
const redis: Indicator = async () => ({ redis: { status: 'up' } });

async function main() {
  console.log(JSON.stringify(await check([db(true), redis])));
  console.log(JSON.stringify(await check([db(false), redis])));
}
main();

Output

{"httpStatus":200,"body":{"status":"ok","info":{"database":{"status":"up"},"redis":{"status":"up"}},"error":{}}}
{"httpStatus":503,"body":{"status":"error","info":{"redis":{"status":"up"}},"error":{"database":{"status":"down","message":"Can't reach database server"}}}}