feat: add api liveness and readiness checks

This commit is contained in:
Bastian Wagner
2026-08-17 13:39:15 +02:00
parent db6d62c303
commit 0815f702d2
12 changed files with 327 additions and 5 deletions

View File

@@ -2,9 +2,10 @@ import { Module } from '@nestjs/common';
import { ConfigurationModule } from '../../../libs/configuration/src';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { HealthModule } from './health/health.module';
@Module({
imports: [ConfigurationModule],
imports: [ConfigurationModule, HealthModule],
controllers: [AppController],
providers: [AppService],
})

View File

@@ -0,0 +1,29 @@
import { Test } from '@nestjs/testing';
import { HealthController } from './health.controller';
import { ReadinessService } from './readiness.service';
describe('HealthController', () => {
it('returns liveness without dependency checks', async () => {
const readiness = { check: jest.fn() };
const moduleRef = await Test.createTestingModule({
controllers: [HealthController],
providers: [{ provide: ReadinessService, useValue: readiness }],
}).compile();
expect(moduleRef.get(HealthController).live()).toEqual({ status: 'ok' });
expect(readiness.check).not.toHaveBeenCalled();
});
it('delegates readiness to dependency checks', async () => {
const readiness = { check: jest.fn().mockResolvedValue({ status: 'ok' }) };
const moduleRef = await Test.createTestingModule({
controllers: [HealthController],
providers: [{ provide: ReadinessService, useValue: readiness }],
}).compile();
await expect(moduleRef.get(HealthController).ready()).resolves.toEqual({
status: 'ok',
});
expect(readiness.check).toHaveBeenCalledTimes(1);
});
});

View File

@@ -0,0 +1,17 @@
import { Controller, Get } from '@nestjs/common';
import { ReadinessService } from './readiness.service';
@Controller()
export class HealthController {
constructor(private readonly readiness: ReadinessService) {}
@Get('health/live')
live(): { status: 'ok' } {
return { status: 'ok' };
}
@Get('health/ready')
ready(): Promise<{ status: 'ok' }> {
return this.readiness.check();
}
}

View File

@@ -0,0 +1,14 @@
import { Module } from '@nestjs/common';
import {
PostgresModule,
RedisModule,
} from '../../../../libs/infrastructure/src';
import { HealthController } from './health.controller';
import { ReadinessService } from './readiness.service';
@Module({
imports: [PostgresModule, RedisModule],
controllers: [HealthController],
providers: [ReadinessService],
})
export class HealthModule {}

View File

@@ -0,0 +1,22 @@
import { Inject, Injectable } from '@nestjs/common';
import type { Pool } from 'pg';
import type Redis from 'ioredis';
import {
POSTGRES_POOL,
REDIS_CLIENT,
} from '../../../../libs/infrastructure/src';
@Injectable()
export class ReadinessService {
constructor(
@Inject(POSTGRES_POOL) private readonly postgresPool: Pool,
@Inject(REDIS_CLIENT) private readonly redis: Redis,
) {}
async check(): Promise<{ status: 'ok' }> {
await this.postgresPool.query('SELECT 1');
const pong = await this.redis.ping();
if (pong !== 'PONG') throw new Error('Redis ping failed');
return { status: 'ok' as const };
}
}

View File

@@ -1,10 +1,16 @@
import { RequestMethod } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { ApiModule } from './api.module';
export async function bootstrapApi(): Promise<void> {
const app = await NestFactory.create(ApiModule);
app.enableShutdownHooks();
app.setGlobalPrefix('api/v1');
app.setGlobalPrefix('api/v1', {
exclude: [
{ path: 'health/live', method: RequestMethod.GET },
{ path: 'health/ready', method: RequestMethod.GET },
],
});
await app.listen(3000, '0.0.0.0');
}

View File

@@ -1,7 +1,8 @@
import { Module } from '@nestjs/common';
import { ConfigurationModule } from '../../../libs/configuration/src';
import { PostgresModule, RedisModule } from '../../../libs/infrastructure/src';
@Module({
imports: [ConfigurationModule],
imports: [ConfigurationModule, PostgresModule, RedisModule],
})
export class WorkerModule {}