feat(api): add health check endpoints with @nestjs/terminus

Add HealthModule with /health (liveness) and /ready (readiness) probes.
Readiness checks DB (Prisma) and Redis connectivity.
Replaces the basic /health endpoint in AppController.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-09 00:33:44 +07:00
parent 3c6ed4c82a
commit 801e29e65c
9 changed files with 212 additions and 5 deletions

View File

@@ -0,0 +1,33 @@
import { Controller, Get } from '@nestjs/common';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { HealthCheck, HealthCheckService } from '@nestjs/terminus';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { PrismaHealthIndicator } from './infrastructure/prisma.health';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { RedisHealthIndicator } from './infrastructure/redis.health';
@Controller()
export class HealthController {
constructor(
private readonly health: HealthCheckService,
private readonly prismaHealth: PrismaHealthIndicator,
private readonly redisHealth: RedisHealthIndicator,
) {}
/** Liveness probe — always returns 200 if the process is running */
@Get('health')
@HealthCheck()
liveness() {
return this.health.check([]);
}
/** Readiness probe — checks DB and Redis connectivity */
@Get('ready')
@HealthCheck()
readiness() {
return this.health.check([
() => this.prismaHealth.isHealthy('database'),
() => this.redisHealth.isHealthy('redis'),
]);
}
}

View File

@@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { TerminusModule } from '@nestjs/terminus';
import { HealthController } from './health.controller';
import { PrismaHealthIndicator } from './infrastructure/prisma.health';
import { RedisHealthIndicator } from './infrastructure/redis.health';
@Module({
imports: [TerminusModule],
controllers: [HealthController],
providers: [PrismaHealthIndicator, RedisHealthIndicator],
})
export class HealthModule {}

View File

@@ -0,0 +1 @@
export { HealthModule } from './health.module';

View File

@@ -0,0 +1,20 @@
import { Injectable } from '@nestjs/common';
import { HealthCheckError, HealthIndicator, type HealthIndicatorResult } from '@nestjs/terminus';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { PrismaService } from '@modules/shared/infrastructure/prisma.service';
@Injectable()
export class PrismaHealthIndicator extends HealthIndicator {
constructor(private readonly prisma: PrismaService) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
await this.prisma.$queryRawUnsafe('SELECT 1');
return this.getStatus(key, true);
} catch {
throw new HealthCheckError('Database check failed', this.getStatus(key, false));
}
}
}

View File

@@ -0,0 +1,25 @@
import { Injectable } from '@nestjs/common';
import { HealthCheckError, HealthIndicator, type HealthIndicatorResult } from '@nestjs/terminus';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { RedisService } from '@modules/shared/infrastructure/redis.service';
@Injectable()
export class RedisHealthIndicator extends HealthIndicator {
constructor(private readonly redis: RedisService) {
super();
}
async isHealthy(key: string): Promise<HealthIndicatorResult> {
try {
const client = this.redis.getClient();
const pong = await client.ping();
const isHealthy = pong === 'PONG';
const result = this.getStatus(key, isHealthy);
if (isHealthy) return result;
throw new HealthCheckError('Redis ping failed', result);
} catch (error) {
if (error instanceof HealthCheckError) throw error;
throw new HealthCheckError('Redis check failed', this.getStatus(key, false));
}
}
}