feat: add api liveness and readiness checks
This commit is contained in:
@@ -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],
|
||||
})
|
||||
|
||||
29
backend/apps/api/src/health/health.controller.spec.ts
Normal file
29
backend/apps/api/src/health/health.controller.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
17
backend/apps/api/src/health/health.controller.ts
Normal file
17
backend/apps/api/src/health/health.controller.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
14
backend/apps/api/src/health/health.module.ts
Normal file
14
backend/apps/api/src/health/health.module.ts
Normal 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 {}
|
||||
22
backend/apps/api/src/health/readiness.service.ts
Normal file
22
backend/apps/api/src/health/readiness.service.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
2
backend/libs/infrastructure/src/index.ts
Normal file
2
backend/libs/infrastructure/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './postgres/postgres.module';
|
||||
export * from './redis/redis.module';
|
||||
27
backend/libs/infrastructure/src/postgres/postgres.module.ts
Normal file
27
backend/libs/infrastructure/src/postgres/postgres.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Inject, Injectable, Module, OnModuleDestroy } from '@nestjs/common';
|
||||
import { Pool } from 'pg';
|
||||
import { APP_ENVIRONMENT, AppEnvironment } from '../../../configuration/src';
|
||||
|
||||
export const POSTGRES_POOL = Symbol('POSTGRES_POOL');
|
||||
|
||||
export const postgresPoolProvider = {
|
||||
provide: POSTGRES_POOL,
|
||||
inject: [APP_ENVIRONMENT],
|
||||
useFactory: (env: AppEnvironment) =>
|
||||
new Pool({ connectionString: env.databaseUrl }),
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
class PostgresLifecycle implements OnModuleDestroy {
|
||||
constructor(@Inject(POSTGRES_POOL) private readonly pool: Pool) {}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
await this.pool.end();
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [postgresPoolProvider, PostgresLifecycle],
|
||||
exports: [postgresPoolProvider],
|
||||
})
|
||||
export class PostgresModule {}
|
||||
27
backend/libs/infrastructure/src/redis/redis.module.ts
Normal file
27
backend/libs/infrastructure/src/redis/redis.module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Inject, Injectable, Module, OnModuleDestroy } from '@nestjs/common';
|
||||
import Redis from 'ioredis';
|
||||
import { APP_ENVIRONMENT, AppEnvironment } from '../../../configuration/src';
|
||||
|
||||
export const REDIS_CLIENT = Symbol('REDIS_CLIENT');
|
||||
|
||||
export const redisClientProvider = {
|
||||
provide: REDIS_CLIENT,
|
||||
inject: [APP_ENVIRONMENT],
|
||||
useFactory: (env: AppEnvironment) =>
|
||||
new Redis(env.redisUrl, { lazyConnect: false }),
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
class RedisLifecycle implements OnModuleDestroy {
|
||||
constructor(@Inject(REDIS_CLIENT) private readonly client: Redis) {}
|
||||
|
||||
async onModuleDestroy(): Promise<void> {
|
||||
await this.client.quit();
|
||||
}
|
||||
}
|
||||
|
||||
@Module({
|
||||
providers: [redisClientProvider, RedisLifecycle],
|
||||
exports: [redisClientProvider],
|
||||
})
|
||||
export class RedisModule {}
|
||||
@@ -26,6 +26,8 @@
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"ioredis": "^6.0.0",
|
||||
"pg": "^8.23.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
@@ -38,6 +40,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.0.0",
|
||||
"@types/pg": "^8.21.0",
|
||||
"@types/supertest": "^7.0.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^10.0.1",
|
||||
@@ -62,8 +65,13 @@
|
||||
],
|
||||
"rootDir": ".",
|
||||
"testRegex": ".*\\.spec\\.ts$",
|
||||
"testPathIgnorePatterns": ["<rootDir>/node_modules/", "<rootDir>/dist/"],
|
||||
"setupFiles": ["<rootDir>/test/setup-env.ts"],
|
||||
"testPathIgnorePatterns": [
|
||||
"<rootDir>/node_modules/",
|
||||
"<rootDir>/dist/"
|
||||
],
|
||||
"setupFiles": [
|
||||
"<rootDir>/test/setup-env.ts"
|
||||
],
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user