Framework Guide
TypeScript API Monitoring
The best way to monitor a TypeScript API is to add type-safe health check endpoints and connect them to an external monitoring service. This guide covers Express, Fastify, and NestJS with fully typed response interfaces.
Why TypeScript APIs Need External Monitoring
TypeScript catches type errors at compile time, but runtime failures are a different story. Your types won't protect you from network timeouts, database connection drops, or memory leaks in production. External monitoring fills this gap.
- Runtime type mismatches -- External API responses that don't match your interfaces can cause silent failures
- Dependency outages -- Database, cache, and third-party service failures cascade through your app
- Memory leaks -- TypeScript doesn't prevent closures and event listeners from leaking memory
- Deployment regressions -- A passing build doesn't guarantee a working production deployment
Type-Safe Health Check Interfaces
Use these TypeScript interfaces as the foundation for all your health check endpoints. They enforce consistent response shapes across every framework.
interface DependencyCheck {
status: 'ok' | 'error';
responseTime: number;
message?: string;
}
interface HealthCheckResponse {
status: 'healthy' | 'unhealthy';
timestamp: string;
uptime: number;
version: string;
checks: Record<string, DependencyCheck>;
}
// Use this for simple liveness probes
interface LivenessResponse {
status: 'healthy';
timestamp: string;
}
Express + TypeScript Health Endpoint
The most common TypeScript API setup. Use typed request and response objects from @types/express for full type safety.
import express, { Request, Response } from 'express';
const app = express();
// Simple liveness check
app.get('/health', (_req: Request, res: Response<LivenessResponse>) => {
res.json({
status: 'healthy',
timestamp: new Date().toISOString(),
});
});
// Comprehensive readiness check
app.get('/ready', async (_req: Request, res: Response<HealthCheckResponse>) => {
const health = await buildHealthCheck();
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
});
Fastify + TypeScript Health Endpoint
Fastify has first-class TypeScript support with built-in schema validation. Use generics on route handlers for end-to-end type safety.
import Fastify from 'fastify';
const app = Fastify({ logger: true });
app.get<{ Reply: HealthCheckResponse }>('/health', async (_request, reply) => {
const health = await buildHealthCheck();
const statusCode = health.status === 'healthy' ? 200 : 503;
reply.status(statusCode).send(health);
});
// With Fastify JSON Schema for runtime validation
app.get('/health', {
schema: {
response: {
200: {
type: 'object',
properties: {
status: { type: 'string', enum: ['healthy'] },
timestamp: { type: 'string' },
uptime: { type: 'number' },
version: { type: 'string' },
checks: { type: 'object' },
},
required: ['status', 'timestamp'],
},
},
},
}, async (_request, reply) => {
const health = await buildHealthCheck();
reply.send(health);
});
NestJS Health Endpoint
NestJS provides the @nestjs/terminus module specifically for health checks. This is the recommended approach for NestJS applications.
import { Controller, Get } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
TypeOrmHealthIndicator,
HttpHealthIndicator,
} from '@nestjs/terminus';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private db: TypeOrmHealthIndicator,
private http: HttpHealthIndicator,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
() => this.db.pingCheck('database'),
() => this.http.pingCheck('api', 'https://api.example.com'),
]);
}
}
Install with: npm install @nestjs/terminus @nestjs/axios
Database Health Checks with Typed Responses
Use this reusable function to check any database connection with proper TypeScript types, timeouts, and error handling.
async function checkDependency(
name: string,
checkFn: () => Promise<void>,
timeoutMs = 5000
): Promise<DependencyCheck> {
const start = Date.now();
try {
await Promise.race([
checkFn(),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('timeout')), timeoutMs)
),
]);
return {
status: 'ok',
responseTime: Date.now() - start,
};
} catch (error) {
return {
status: 'error',
responseTime: Date.now() - start,
message: error instanceof Error ? error.message : 'Unknown error',
};
}
}
// Usage with any database client
async function buildHealthCheck(): Promise<HealthCheckResponse> {
const dbCheck = await checkDependency('database', async () => {
await db.query('SELECT 1');
});
const redisCheck = await checkDependency('redis', async () => {
await redis.ping();
});
const checks = { database: dbCheck, redis: redisCheck };
const isHealthy = Object.values(checks).every(c => c.status === 'ok');
return {
status: isHealthy ? 'healthy' : 'unhealthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.npm_package_version || '1.0.0',
checks,
};
}
Comparing Health Check Approaches
| Approach | Type Safety | Runtime Validation | Best For |
|---|---|---|---|
| Interfaces only | Compile-time | None | Simple APIs, internal services |
| Zod schemas | Compile + runtime | Full | Public APIs, strict contracts |
| Fastify schemas | Compile + runtime | JSON Schema | Fastify projects |
| NestJS Terminus | Full (decorators) | Built-in | NestJS projects |
Set Up Monitoring with UptimeSignal
Once your health endpoint is deployed, connect it to UptimeSignal for continuous external monitoring. UptimeSignal pings your endpoint on a schedule and alerts you immediately when it returns a non-200 status or times out.
- Sign up at app.uptimesignal.io -- free tier includes 25 monitors
- Add your health endpoint URL -- e.g.,
https://api.example.com/health - Set your check interval -- 5 minutes on free, 1 minute on Pro ($15/mo)
- Configure alerts -- get email notifications when your API goes down or recovers
UptimeSignal verifies both the HTTP status code (expects 200) and response time. If your buildHealthCheck() function detects an unhealthy dependency and returns 503, UptimeSignal will immediately alert you.
Best Practices
- Separate liveness from readiness -- Use
/healthfor simple "is the process alive" checks and/readyfor dependency checks - Add timeouts to every dependency check -- Use
Promise.racewith a 5-second timeout to prevent hanging - Return 503 for unhealthy states -- Monitoring services like UptimeSignal key on status codes
- Keep health endpoints unauthenticated -- Monitoring services need direct access without credentials
- Use strict TypeScript config -- Enable
strict: truein tsconfig.json to catch potential null/undefined issues in health check logic - Log health check traffic separately -- Exclude
/healthfrom your request logger to avoid noise