@betternest/health
v3.1.0
Published
Auto-discovery health checks with decorators for NestJS
Maintainers
Readme
@betternest/health
Auto-discovery health checks with decorators for NestJS
@betternest/health provides auto-discovery health checks for your NestJS application. Simply decorate your service methods with @HealthCheck() and create your own controller to expose them however you want.
Features
- ✅ Auto-Discovery - Automatically finds all health checks
- ✅ No Built-in Controller - You create your own endpoints
- ✅ Flexible Endpoint Design - Design health checks your way
- ✅ Two Methods -
isHealthy()(lightweight) andgetHealth()(detailed) - ✅ Type-Safe - Full TypeScript support
- ✅ Version Info - Includes app version in response
Installation
npm install @betternest/health
# or
yarn add @betternest/health
# or
pnpm add @betternest/healthQuick Start
1. Import Module
import { Module } from '@nestjs/common';
import { BetterHealthModule } from '@betternest/health';
@Module({
imports: [BetterHealthModule],
})
export class AppModule {}2. Add Health Checks to Your Services
import { Injectable } from '@nestjs/common';
import { HealthCheck } from '@betternest/health';
@Injectable()
export class DatabaseService {
@HealthCheck()
protected async checkConnection() {
const isConnected = await this.database.ping();
if (!isConnected) {
throw new Error('Database not connected');
}
return {
connected: true,
latency: await this.database.getLatency(),
};
}
}
@Injectable()
export class CacheService {
@HealthCheck()
protected async checkCache() {
return {
connected: await this.redis.ping(),
memory: await this.redis.info('memory'),
};
}
}3. Create Your Controller
import { Controller, Get, HttpException, HttpStatus, Param, UseGuards } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiOkResponse, ApiInternalServerErrorResponse, ApiBearerAuth, ApiParam } from '@nestjs/swagger';
import { HealthService } from '@betternest/health';
import { Public } from './decorators/public.decorator';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
interface HealthResponse {
ok: boolean;
}
interface DiagnosticsResponse {
hasError: boolean;
states: Record<string, 'OK' | 'ERRORED'>;
services: Record<string, unknown>;
version: string;
timestamp: string;
}
@Controller()
@ApiTags('Health')
@UseGuards(JwtAuthGuard)
export class HealthController {
constructor(private readonly healthService: HealthService) {}
@Get('healthz')
@Public()
@ApiOperation({ summary: 'Lightweight health check for Kubernetes probes' })
@ApiOkResponse({ description: 'All services healthy', type: Object })
@ApiInternalServerErrorResponse({ description: 'One or more services unhealthy' })
async healthz(): Promise<HealthResponse> {
const isHealthy = await this.healthService.isHealthy();
if (!isHealthy) {
throw new HttpException('Unhealthy', HttpStatus.INTERNAL_SERVER_ERROR);
}
return { ok: true };
}
@Get('diagnostics')
@ApiBearerAuth()
@ApiOperation({ summary: 'Detailed health diagnostics (requires authentication)' })
@ApiOkResponse({ description: 'Health diagnostics retrieved', type: Object })
@ApiInternalServerErrorResponse({ description: 'One or more services unhealthy' })
async diagnostics(): Promise<DiagnosticsResponse> {
const health = await this.healthService.getHealth();
if (health.hasError) {
throw new HttpException(health, HttpStatus.INTERNAL_SERVER_ERROR);
}
return health;
}
@Get('diagnostics/:services')
@ApiBearerAuth()
@ApiOperation({ summary: 'Detailed health diagnostics for specific service(s) (requires authentication)' })
@ApiParam({ name: 'services', description: 'Comma-separated service names (e.g., DatabaseService,CacheService)' })
@ApiOkResponse({ description: 'Service health diagnostics retrieved', type: Object })
@ApiInternalServerErrorResponse({ description: 'Service unhealthy or not found' })
async diagnosticsForServices(@Param('services') services: string): Promise<DiagnosticsResponse> {
const serviceList = services.split(',').map(s => s.trim());
const health = await this.healthService.getHealth(serviceList);
if (health.hasError) {
throw new HttpException(health, HttpStatus.INTERNAL_SERVER_ERROR);
}
return health;
}
}Security Considerations
Why Two Endpoints?
/healthz - Public, Minimal
- ✅ Designed for Kubernetes liveness/readiness probes
- ✅ Returns only
{ ok: true }- no sensitive information - ✅ Fast and lightweight (uses
isHealthy()) - ✅ Public access (no authentication required)
/diagnostics - Protected, Detailed
- ⚠️ Returns full diagnostic information
- ⚠️ May expose:
- Database connection details
- Redis memory usage
- API latencies
- Service versions
- Error messages
- 🔒 Must be protected with authentication
- 🔒 Use for internal monitoring/debugging only
What Information is Exposed?
The getHealth() method returns detailed information:
{
"hasError": false,
"states": {
"DatabaseService": "OK",
"CacheService": "OK"
},
"services": {
"DatabaseService": {
"connected": true,
"latency": 5,
"pool": { "active": 10, "idle": 5 }
},
"CacheService": {
"connected": true,
"memory": "used_memory:1024"
}
},
"version": "1.0.0",
"timestamp": "2025-01-05T12:00:00.000Z"
}Protect this information - it can reveal infrastructure details to attackers.
Usage
Basic Health Check
@Injectable()
export class MyService {
@HealthCheck()
protected async check() {
return {
status: 'OK',
uptime: process.uptime(),
};
}
}Health Check with Error
@Injectable()
export class ApiService {
@HealthCheck()
protected async checkApi() {
const response = await fetch('https://api.example.com/health');
if (!response.ok) {
throw new Error(`API returned ${response.status}`);
}
return {
status: 'OK',
latency: response.headers.get('x-response-time'),
};
}
}Health Check with Structured Error Data
Use HealthError to return structured data even on failure:
import { HealthCheck, HealthError } from '@betternest/health';
@Injectable()
export class DatabaseService {
@HealthCheck()
protected async checkDatabase() {
try {
await this.database.ping();
return {
connected: true,
pool: await this.database.getPoolStatus(),
};
} catch (error) {
// Return structured error data
throw new HealthError({
connected: false,
error: error.message,
lastSuccessfulPing: this.lastPingTime,
});
}
}
}Response when using HealthError:
{
"hasError": true,
"states": {
"DatabaseService": "ERRORED"
},
"services": {
"DatabaseService": {
"connected": false,
"error": "Connection timeout",
"lastSuccessfulPing": "2025-01-05T11:00:00.000Z"
}
}
}Best Practices
1. Use Protected Methods
Health checks should be protected (not public) to keep them internal:
@HealthCheck()
protected async check() { // ✅ Good
return { status: 'OK' };
}
@HealthCheck()
async check() { // ⚠️ Works but less encapsulated
return { status: 'OK' };
}2. One Health Check Per Service
Each service should have exactly ONE @HealthCheck() method:
// ✅ Good
@Injectable()
export class DatabaseService {
@HealthCheck()
protected async check() {
return {
connected: await this.checkConnection(),
migrations: await this.checkMigrations(),
};
}
}
// ❌ Bad - Multiple health checks in one service
@Injectable()
export class DatabaseService {
@HealthCheck()
protected async checkConnection() { }
@HealthCheck() // ERROR: Duplicate health check!
protected async checkMigrations() { }
}3. Keep Checks Fast
Health checks should be fast (<1 second):
// ✅ Good - Fast ping
@HealthCheck()
protected async check() {
return {
connected: await this.redis.ping(), // ~1-5ms
};
}
// ❌ Bad - Slow query
@HealthCheck()
protected async check() {
return {
totalUsers: await this.db.users.count(), // Could be slow!
};
}4. Return Useful Information
Include diagnostic information in your health checks:
@HealthCheck()
protected async check() {
return {
connected: true,
latency: 5, // ✅ Useful
connections: { // ✅ Useful
active: 10,
idle: 5,
total: 15,
},
lastError: null, // ✅ Useful
version: this.getVersion(), // ✅ Useful
};
}Advanced Usage
Kubernetes Liveness/Readiness Probes
Use the lightweight /healthz endpoint:
# kubernetes/deployment.yaml
spec:
containers:
- name: app
livenessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 10
periodSeconds: 30
readinessProbe:
httpGet:
path: /healthz
port: 3000
initialDelaySeconds: 5
periodSeconds: 10Programmatic Health Checks
Inject HealthService to check health programmatically:
import { Injectable } from '@nestjs/common';
import { HealthService } from '@betternest/health';
@Injectable()
export class MonitoringService {
constructor(
private readonly healthService: HealthService,
) {}
async getApplicationHealth() {
const health = await this.healthService.getHealth();
if (health.hasError) {
await this.sendAlert('Application health degraded', health);
}
return health;
}
async isApplicationHealthy() {
return this.healthService.isHealthy();
}
async getAvailableServices() {
// Get list of all services with health checks
return this.healthService.getHealthCheckNames();
}
async checkSpecificService(serviceName: string) {
return this.healthService.getHealth(serviceName);
}
async checkMultipleServices(serviceNames: string[]) {
return this.healthService.getHealth(serviceNames);
}
}Discovering Available Services
Use getHealthCheckNames() to get a list of all registered health check services:
const services = await healthService.getHealthCheckNames();
// Returns: ['DatabaseService', 'CacheService', 'ApiService']This is useful for:
- Building dynamic monitoring UIs
- Validating service names before querying
- Auto-generating documentation
- Debugging registration issues
Filtering Health Checks
You can filter health checks to specific services:
// Check all services
const allHealth = await healthService.getHealth();
// Check single service (string - simple and concise)
const dbHealth = await healthService.getHealth('DatabaseService');
// Check multiple services (array)
const multiHealth = await healthService.getHealth(['DatabaseService', 'CacheService']);Response when filtering:
{
"hasError": false,
"states": {
"DatabaseService": "OK",
"CacheService": "OK"
},
"services": {
"DatabaseService": { "connected": true, "latency": 5 },
"CacheService": { "connected": true, "memory": "used_memory:1024" }
},
"version": "1.0.0",
"timestamp": "2025-01-05T12:00:00.000Z"
}Non-existent service:
If you request a service that doesn't exist, it will be marked as ERRORED:
{
"hasError": true,
"states": {
"NonExistentService": "ERRORED"
},
"services": {
"NonExistentService": "Health check not found"
},
"version": "1.0.0",
"timestamp": "2025-01-05T12:00:00.000Z"
}Comparison with @nestjs/terminus
| Feature | @betternest/health | @nestjs/terminus | |---------|------------------------|------------------| | Setup | ⚠️ Minimal (create controller) | ⚠️ Manual setup required | | Auto-discovery | ✅ Yes | ❌ No | | API | ✅ Simple decorator | ⚠️ Verbose HealthService | | Controller | ⚠️ You create | ✅ Built-in | | Type-safety | ✅ Full | ✅ Full | | Built-in indicators | ❌ DIY | ✅ Many built-in | | Custom indicators | ✅ Easy | ✅ Possible |
When to use @betternest/health:
- ✅ You want auto-discovery
- ✅ You prefer decorators over imperative code
- ✅ You want full control over endpoints
- ✅ You have custom health check logic
When to use @nestjs/terminus:
- ✅ You need built-in indicators (disk, memory, etc.)
- ✅ You want battle-tested library
- ✅ You need complex health check composition
- ✅ You prefer built-in controller
Examples
See the examples directory for complete working examples.
Troubleshooting
Health check not discovered
- Ensure
BetterHealthModuleis imported - Verify method is decorated with
@HealthCheck() - Check that the service is properly registered as a provider
- Check logs for "Health check registered: ServiceName"
Multiple health checks error
Each service can only have ONE @HealthCheck() method. Combine multiple checks into one method:
@HealthCheck()
protected async check() {
return {
database: await this.checkDatabase(),
cache: await this.checkCache(),
api: await this.checkApi(),
};
}Contributing
Contributions are welcome! Please see CONTRIBUTING.md.
License
MIT © Mathieu Colmon
Related Packages
- @betternest/config - Type-safe configuration
- @betternest/workflows - MongoDB-based workflow orchestration
Part of the BetterNest ecosystem - Production-proven patterns for NestJS applications.
