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:
33
apps/api/src/modules/health/health.controller.ts
Normal file
33
apps/api/src/modules/health/health.controller.ts
Normal 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'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
12
apps/api/src/modules/health/health.module.ts
Normal file
12
apps/api/src/modules/health/health.module.ts
Normal 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 {}
|
||||
1
apps/api/src/modules/health/index.ts
Normal file
1
apps/api/src/modules/health/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { HealthModule } from './health.module';
|
||||
20
apps/api/src/modules/health/infrastructure/prisma.health.ts
Normal file
20
apps/api/src/modules/health/infrastructure/prisma.health.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
25
apps/api/src/modules/health/infrastructure/redis.health.ts
Normal file
25
apps/api/src/modules/health/infrastructure/redis.health.ts
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user