How to Build an API Status Dashboard: Complete Tutorial with Code Examples
Quick Answer
How do I build an API status dashboard? Create a Next.js application with API routes that periodically check endpoint health, store results in Redis for caching and PostgreSQL for historical data, display real-time status with WebSocket updates, and send alerts via email/Slack when services go down. The core components are: health checkers, data storage, real-time UI updates, and alerting logic.
Architecture Overview
A production-ready API status dashboard consists of four layers:
1. Health Check Layer
Periodically pings API endpoints, checks response times, validates status codes, and detects anomalies. Runs on a scheduled interval (every 1-5 minutes).
2. Data Layer
- Redis: Caches current status for instant reads
- PostgreSQL: Stores historical health check results for trend analysis
- Time-series optimization: Indexes and partitioning for efficient queries
3. Real-Time Layer
- WebSocket connections push updates to browser clients immediately
- Server-Sent Events (SSE) as a simpler alternative
- Polling fallback for maximum compatibility
4. Alerting Layer
Detects status changes and triggers notifications via:
- Email (transactional email services)
- Slack/Discord webhooks
- SMS (Twilio)
- Custom webhooks for integration with PagerDuty, Opsgenie, etc.
Visual Flow
[Scheduled Jobs] → [Health Checkers] → [Data Layer]
↓
[Browser Client] ← [WebSocket/SSE] ← [API Routes]
↓
[Alert System]
Tech Stack Options
Frontend Framework
Next.js (Recommended)
- Server-side rendering for fast initial loads
- API routes eliminate need for separate backend
- Built-in image optimization and caching
- Easy deployment to Vercel
React + Vite
- Lighter-weight alternative
- Faster development builds
- Requires separate API server
Vue/Svelte
- Both work great for dashboards
- Choose based on team familiarity
Backend Runtime
Node.js with TypeScript
- Same language as frontend
- Excellent async handling for HTTP requests
- Rich ecosystem of monitoring libraries
Python with FastAPI
- Great for complex data processing
- Strong data science libraries for anomaly detection
- Async support with asyncio
Database
Redis
- Store current status (key:
service:{id}:status, value:{"status":"up","latency":45}) - Cache aggregated metrics
- Pub/sub for real-time updates
- TTL for automatic cleanup
PostgreSQL
- Store historical check results
- Time-series tables with partitioning
- Rich querying for trends and reports
TimescaleDB (PostgreSQL extension)
- Optimized for time-series data
- Automatic data retention policies
- Better compression and query performance
Step-by-Step Tutorial
Let's build a production-ready API status dashboard using Next.js, TypeScript, Redis, and PostgreSQL.
Prerequisites
# Install Node.js 18+ and pnpm
node --version # v18.0.0 or higher
pnpm --version # 8.0.0 or higher
# Install Docker for local Redis/Postgres
docker --version
Step 1: Project Setup
# Create Next.js project with TypeScript
pnpm create next-app@latest api-status-dashboard \
--typescript \
--tailwind \
--app \
--src-dir
cd api-status-dashboard
# Install dependencies
pnpm add ioredis pg date-fns recharts ws
pnpm add -D @types/pg @types/ws prisma
# Initialize Prisma
pnpx prisma init
Step 2: Database Schema
Create prisma/schema.prisma:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model Service {
id String @id @default(cuid())
name String
url String
type String // 'rest', 'graphql', 'grpc'
checkInterval Int @default(300000) // 5 minutes in ms
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
checks Check[]
incidents Incident[]
@@index([name])
}
model Check {
id String @id @default(cuid())
serviceId String
status String // 'up', 'down', 'degraded'
latency Int // milliseconds
statusCode Int?
errorMessage String?
checkedAt DateTime @default(now())
service Service @relation(fields: [serviceId], references: [id])
@@index([serviceId, checkedAt(sort: Desc)])
@@index([checkedAt(sort: Desc)])
}
model Incident {
id String @id @default(cuid())
serviceId String
status String // 'investigating', 'identified', 'monitoring', 'resolved'
title String
description String?
startedAt DateTime @default(now())
resolvedAt DateTime?
service Service @relation(fields: [serviceId], references: [id])
updates IncidentUpdate[]
@@index([serviceId])
@@index([startedAt(sort: Desc)])
}
model IncidentUpdate {
id String @id @default(cuid())
incidentId String
status String
message String
createdAt DateTime @default(now())
incident Incident @relation(fields: [incidentId], references: [id])
@@index([incidentId])
}
Run migrations:
# Set your database URL
echo 'DATABASE_URL="postgresql://user:password@localhost:5432/status_dashboard"' > .env
# Run migrations
pnpx prisma migrate dev --name init
pnpx prisma generate
Step 3: Redis Client
Create src/lib/redis.ts:
// src/lib/redis.ts
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379', {
maxRetriesPerRequest: 3,
retryStrategy: (times) => {
const delay = Math.min(times * 50, 2000);
return delay;
},
});
redis.on('error', (err) => console.error('Redis error:', err));
redis.on('connect', () => console.log('Redis connected'));
export default redis;
// Helper functions
export async function cacheServiceStatus(serviceId: string, data: any) {
await redis.setex(
`service:${serviceId}:status`,
300, // 5 minutes TTL
JSON.stringify(data)
);
}
export async function getCachedServiceStatus(serviceId: string) {
const cached = await redis.get(`service:${serviceId}:status`);
return cached ? JSON.parse(cached) : null;
}
export async function publishStatusUpdate(serviceId: string, data: any) {
await redis.publish('status:updates', JSON.stringify({ serviceId, ...data }));
}
Step 4: Health Check Engine
Create src/lib/health-checker.ts:
// src/lib/health-checker.ts
import { PrismaClient } from '@prisma/client';
import { cacheServiceStatus, publishStatusUpdate } from './redis';
const prisma = new PrismaClient();
interface CheckResult {
status: 'up' | 'down' | 'degraded';
latency: number;
statusCode?: number;
errorMessage?: string;
}
export async function checkEndpoint(url: string): Promise<CheckResult> {
const startTime = Date.now();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10s timeout
const response = await fetch(url, {
method: 'GET',
signal: controller.signal,
headers: {
'User-Agent': 'StatusDashboard/1.0',
},
});
clearTimeout(timeoutId);
const latency = Date.now() - startTime;
const status = response.ok
? (latency > 1000 ? 'degraded' : 'up')
: 'down';
return {
status,
latency,
statusCode: response.status,
};
} catch (error: any) {
return {
status: 'down',
latency: Date.now() - startTime,
errorMessage: error.message,
};
}
}
export async function checkService(serviceId: string) {
const service = await prisma.service.findUnique({
where: { id: serviceId },
});
if (!service) {
throw new Error(`Service ${serviceId} not found`);
}
const result = await checkEndpoint(service.url);
// Save to database
const check = await prisma.check.create({
data: {
serviceId: service.id,
status: result.status,
latency: result.latency,
statusCode: result.statusCode,
errorMessage: result.errorMessage,
},
});
// Cache current status
await cacheServiceStatus(serviceId, {
status: result.status,
latency: result.latency,
lastChecked: check.checkedAt,
});
// Publish real-time update
await publishStatusUpdate(serviceId, {
status: result.status,
latency: result.latency,
timestamp: check.checkedAt,
});
// Check if we need to create an incident
await handleIncidents(service.id, result.status);
return check;
}
async function handleIncidents(serviceId: string, currentStatus: string) {
// Find open incidents
const openIncident = await prisma.incident.findFirst({
where: {
serviceId,
resolvedAt: null,
},
});
if (currentStatus === 'down' && !openIncident) {
// Create new incident
await prisma.incident.create({
data: {
serviceId,
status: 'investigating',
title: 'Service Unavailable',
description: 'Automated detection of service outage',
},
});
} else if (currentStatus === 'up' && openIncident) {
// Resolve incident
await prisma.incident.update({
where: { id: openIncident.id },
data: {
status: 'resolved',
resolvedAt: new Date(),
},
});
}
}
export async function checkAllServices() {
const services = await prisma.service.findMany();
const results = await Promise.allSettled(
services.map(service => checkService(service.id))
);
return results;
}
Step 5: API Routes
Create src/app/api/services/route.ts:
// src/app/api/services/route.ts
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
import { getCachedServiceStatus } from '@/lib/redis';
const prisma = new PrismaClient();
export async function GET() {
try {
const services = await prisma.service.findMany({
orderBy: { name: 'asc' },
});
// Enrich with cached status
const enrichedServices = await Promise.all(
services.map(async (service) => {
const cachedStatus = await getCachedServiceStatus(service.id);
if (cachedStatus) {
return { ...service, currentStatus: cachedStatus };
}
// Fallback to latest check in DB
const latestCheck = await prisma.check.findFirst({
where: { serviceId: service.id },
orderBy: { checkedAt: 'desc' },
});
return {
...service,
currentStatus: latestCheck ? {
status: latestCheck.status,
latency: latestCheck.latency,
lastChecked: latestCheck.checkedAt,
} : null,
};
})
);
return NextResponse.json(enrichedServices);
} catch (error) {
console.error('Error fetching services:', error);
return NextResponse.json(
{ error: 'Failed to fetch services' },
{ status: 500 }
);
}
}
export async function POST(request: Request) {
try {
const body = await request.json();
const { name, url, type, checkInterval } = body;
const service = await prisma.service.create({
data: {
name,
url,
type: type || 'rest',
checkInterval: checkInterval || 300000,
},
});
return NextResponse.json(service, { status: 201 });
} catch (error) {
console.error('Error creating service:', error);
return NextResponse.json(
{ error: 'Failed to create service' },
{ status: 500 }
);
}
}
Create src/app/api/services/[id]/history/route.ts:
// src/app/api/services/[id]/history/route.ts
import { NextResponse } from 'next/server';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
try {
const { searchParams } = new URL(request.url);
const hours = parseInt(searchParams.get('hours') || '24');
const since = new Date(Date.now() - hours * 60 * 60 * 1000);
const checks = await prisma.check.findMany({
where: {
serviceId: params.id,
checkedAt: { gte: since },
},
orderBy: { checkedAt: 'desc' },
take: 500,
});
// Calculate uptime percentage
const totalChecks = checks.length;
const upChecks = checks.filter(c => c.status === 'up').length;
const uptime = totalChecks > 0 ? (upChecks / totalChecks) * 100 : 0;
return NextResponse.json({
checks,
uptime: uptime.toFixed(2),
totalChecks,
period: `${hours}h`,
});
} catch (error) {
console.error('Error fetching history:', error);
return NextResponse.json(
{ error: 'Failed to fetch history' },
{ status: 500 }
);
}
}
Step 6: Scheduled Health Checks
Create src/app/api/cron/health-check/route.ts:
// src/app/api/cron/health-check/route.ts
import { NextResponse } from 'next/server';
import { checkAllServices } from '@/lib/health-checker';
export async function GET(request: Request) {
// Verify cron secret to prevent unauthorized calls
const authHeader = request.headers.get('authorization');
if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
try {
const results = await checkAllServices();
return NextResponse.json({
success: true,
checked: results.length,
timestamp: new Date().toISOString(),
});
} catch (error) {
console.error('Health check cron error:', error);
return NextResponse.json(
{ error: 'Health check failed' },
{ status: 500 }
);
}
}
// For Vercel cron jobs
export const runtime = 'nodejs';
export const maxDuration = 300; // 5 minutes
Configure in vercel.json:
{
"crons": [{
"path": "/api/cron/health-check",
"schedule": "*/5 * * * *"
}]
}
Step 7: Real-Time WebSocket Updates
Create src/app/api/websocket/route.ts:
// src/app/api/websocket/route.ts (for custom server)
// For Next.js on Vercel, use SSE instead (see below)
import { Server } from 'ws';
import redis from '@/lib/redis';
let wss: Server;
export function initWebSocket(server: any) {
if (wss) return wss;
wss = new Server({ server, path: '/api/ws' });
// Subscribe to Redis pub/sub
const subscriber = redis.duplicate();
subscriber.subscribe('status:updates');
subscriber.on('message', (channel, message) => {
// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === 1) { // OPEN
client.send(message);
}
});
});
wss.on('connection', (ws) => {
console.log('WebSocket client connected');
ws.on('close', () => {
console.log('WebSocket client disconnected');
});
});
return wss;
}
Alternative: Server-Sent Events (SSE) for Vercel:
Create src/app/api/sse/route.ts:
// src/app/api/sse/route.ts
import redis from '@/lib/redis';
export async function GET() {
const encoder = new TextEncoder();
const stream = new ReadableStream({
async start(controller) {
const subscriber = redis.duplicate();
await subscriber.subscribe('status:updates');
subscriber.on('message', (channel, message) => {
const data = `data: ${message}\n\n`;
controller.enqueue(encoder.encode(data));
});
// Send heartbeat every 30s
const interval = setInterval(() => {
controller.enqueue(encoder.encode(': heartbeat\n\n'));
}, 30000);
// Cleanup on close
return () => {
clearInterval(interval);
subscriber.unsubscribe();
subscriber.quit();
};
},
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
});
}
export const runtime = 'nodejs';
Step 8: Frontend Dashboard
Create src/app/page.tsx:
// src/app/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { formatDistanceToNow } from 'date-fns';
interface Service {
id: string;
name: string;
url: string;
currentStatus: {
status: 'up' | 'down' | 'degraded';
latency: number;
lastChecked: string;
} | null;
}
export default function Dashboard() {
const [services, setServices] = useState<Service[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Initial load
fetchServices();
// Set up SSE connection for real-time updates
const eventSource = new EventSource('/api/sse');
eventSource.onmessage = (event) => {
try {
const update = JSON.parse(event.data);
updateServiceStatus(update.serviceId, update);
} catch (err) {
console.error('SSE parse error:', err);
}
};
eventSource.onerror = () => {
console.error('SSE connection error');
eventSource.close();
};
return () => {
eventSource.close();
};
}, []);
async function fetchServices() {
try {
const res = await fetch('/api/services');
const data = await res.json();
setServices(data);
} catch (error) {
console.error('Failed to fetch services:', error);
} finally {
setLoading(false);
}
}
function updateServiceStatus(serviceId: string, statusData: any) {
setServices(prev =>
prev.map(service =>
service.id === serviceId
? {
...service,
currentStatus: {
status: statusData.status,
latency: statusData.latency,
lastChecked: statusData.timestamp,
},
}
: service
)
);
}
if (loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-xl">Loading services...</div>
</div>
);
}
const overallStatus = services.every(s => s.currentStatus?.status === 'up')
? 'All Systems Operational'
: services.some(s => s.currentStatus?.status === 'down')
? 'Partial Outage'
: 'Degraded Performance';
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-6xl mx-auto px-4 py-8">
<header className="mb-8">
<h1 className="text-4xl font-bold mb-2">API Status Dashboard</h1>
<div className="flex items-center gap-3">
<StatusBadge status={overallStatus} />
<span className="text-gray-600">
Last updated: {new Date().toLocaleString()}
</span>
</div>
</header>
<div className="space-y-4">
{services.map(service => (
<ServiceCard key={service.id} service={service} />
))}
</div>
</div>
</div>
);
}
function ServiceCard({ service }: { service: Service }) {
const status = service.currentStatus;
return (
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-3">
<StatusIndicator status={status?.status || 'unknown'} />
<h3 className="text-xl font-semibold">{service.name}</h3>
</div>
{status && (
<div className="text-sm text-gray-500">
{status.latency}ms • {formatDistanceToNow(new Date(status.lastChecked))} ago
</div>
)}
</div>
<div className="text-sm text-gray-600">{service.url}</div>
</div>
);
}
function StatusIndicator({ status }: { status: string }) {
const colors = {
up: 'bg-green-500',
down: 'bg-red-500',
degraded: 'bg-yellow-500',
unknown: 'bg-gray-400',
};
return (
<div className={`w-3 h-3 rounded-full ${colors[status as keyof typeof colors]}`} />
);
}
function StatusBadge({ status }: { status: string }) {
const isOperational = status === 'All Systems Operational';
return (
<span className={`px-4 py-2 rounded-full text-sm font-medium ${
isOperational
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}>
{status}
</span>
);
}
Step 9: Alert System
Create src/lib/alerts.ts:
// src/lib/alerts.ts
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
interface AlertConfig {
email?: string[];
slack?: string; // Webhook URL
webhook?: string; // Custom webhook URL
}
export async function sendAlert(
serviceId: string,
status: 'down' | 'up',
config: AlertConfig
) {
const service = await prisma.service.findUnique({
where: { id: serviceId },
});
if (!service) return;
const message = status === 'down'
? `🔴 ${service.name} is DOWN`
: `✅ ${service.name} is back UP`;
const promises = [];
// Email alerts
if (config.email && config.email.length > 0) {
promises.push(sendEmailAlert(config.email, service.name, status));
}
// Slack alerts
if (config.slack) {
promises.push(sendSlackAlert(config.slack, message, service));
}
// Custom webhook
if (config.webhook) {
promises.push(sendWebhookAlert(config.webhook, {
service: service.name,
status,
timestamp: new Date().toISOString(),
url: service.url,
}));
}
await Promise.allSettled(promises);
}
async function sendEmailAlert(
recipients: string[],
serviceName: string,
status: string
) {
// Integrate with Resend, SendGrid, or your email provider
const response = await fetch('https://api.resend.com/emails', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.RESEND_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
from: 'alerts@yourdomain.com',
to: recipients,
subject: `Alert: ${serviceName} is ${status}`,
html: `
<h2>${serviceName} Status Change</h2>
<p>Current status: <strong>${status}</strong></p>
<p>Time: ${new Date().toLocaleString()}</p>
`,
}),
});
return response.json();
}
async function sendSlackAlert(
webhookUrl: string,
message: string,
service: any
) {
const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: message,
attachments: [{
color: service.currentStatus === 'down' ? 'danger' : 'good',
fields: [
{ title: 'Service', value: service.name, short: true },
{ title: 'URL', value: service.url, short: true },
],
}],
}),
});
return response.text();
}
async function sendWebhookAlert(url: string, payload: any) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
});
return response.text();
}
Integrate alerts in the health checker:
// Add to src/lib/health-checker.ts
import { sendAlert } from './alerts';
// Inside handleIncidents function:
if (currentStatus === 'down' && !openIncident) {
await sendAlert(serviceId, 'down', {
email: ['ops@yourcompany.com'],
slack: process.env.SLACK_WEBHOOK_URL,
});
} else if (currentStatus === 'up' && openIncident) {
await sendAlert(serviceId, 'up', {
email: ['ops@yourcompany.com'],
slack: process.env.SLACK_WEBHOOK_URL,
});
}
Deployment Options
1. Vercel (Recommended for Next.js)
# Install Vercel CLI
pnpm add -g vercel
# Set environment variables
vercel env add DATABASE_URL
vercel env add REDIS_URL
vercel env add CRON_SECRET
vercel env add RESEND_API_KEY
vercel env add SLACK_WEBHOOK_URL
# Deploy
vercel --prod
Managed Services:
- Database: Neon or Supabase (PostgreSQL)
- Redis: Upstash (serverless Redis)
- Monitoring: Vercel built-in analytics
2. Railway
Railway provides a seamless deployment experience with built-in databases:
# Install Railway CLI
npm i -g @railway/cli
# Login and initialize
railway login
railway init
# Add PostgreSQL and Redis
railway add --plugin postgresql
railway add --plugin redis
# Deploy
railway up
Railway automatically provisions databases and injects environment variables.
3. Self-Hosted (Docker Compose)
Create docker-compose.yml:
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- DATABASE_URL=postgresql://postgres:password@db:5432/status
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
db:
image: postgres:15
environment:
- POSTGRES_PASSWORD=password
- POSTGRES_DB=status
volumes:
- postgres-data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
- redis-data:/data
volumes:
postgres-data:
redis-data:
Deploy:
docker-compose up -d
4. DigitalOcean App Platform
# Create app.yaml
doctl apps create --spec app.yaml
Example app.yaml:
name: api-status-dashboard
region: nyc
databases:
- name: postgres
engine: PG
production: true
- name: redis
engine: REDIS
production: true
services:
- name: web
source_dir: /
github:
repo: your-username/api-status-dashboard
branch: main
build_command: pnpm install && pnpm build
run_command: pnpm start
envs:
- key: DATABASE_URL
scope: RUN_TIME
type: SECRET
- key: REDIS_URL
scope: RUN_TIME
type: SECRET
Or Just Use API Status Check!
Building and maintaining a production-grade API status dashboard requires significant effort:
- Initial development: 40-80 hours
- Infrastructure costs: $50-200/month
- Ongoing maintenance: 5-10 hours/month
- Scaling challenges: Database optimization, rate limiting, global monitoring
- Alert fatigue: Fine-tuning thresholds to avoid false positives
We've already solved these problems.
API Status Check provides:
✅ Pre-built monitoring for 100+ popular APIs (Stripe, OpenAI, AWS, etc.)
✅ Real-time dashboard with historical uptime tracking
✅ Smart alerts via email, Slack, Discord, and webhooks
✅ 5-minute setup — no code required
✅ Generous free tier — start monitoring today
When to Build vs. Buy
Build your own if:
- You need to monitor internal/private APIs only
- You have specific compliance requirements
- You want full control over data retention
- You have engineering time to spare
Use API Status Check if:
- You monitor third-party APIs (Stripe, Twilio, OpenAI, etc.)
- You want to ship fast and avoid maintenance
- You need multi-region monitoring
- You want a proven solution with 99.9% uptime
Start monitoring in 5 minutes →
FAQs
1. How often should I check API health?
For critical services: Every 1-2 minutes
For standard services: Every 5 minutes
For low-priority services: Every 15-30 minutes
More frequent checks provide faster incident detection but increase infrastructure costs and may trigger rate limits on monitored APIs.
2. What's the best way to check if an API is truly down?
Use a multi-region strategy. Check from at least 2-3 geographic locations to distinguish between:
- Network issues (one region fails, others succeed)
- True outages (all regions fail)
- Rate limiting (403/429 errors)
Also implement synthetic transactions that test full workflows, not just ping endpoints.
3. How do I avoid alert fatigue?
Implement smart alerting:
- Threshold: Alert only after 2-3 consecutive failures
- Cooldown: Don't re-alert for 15-30 minutes after first alert
- Escalation: Start with Slack, escalate to SMS/PagerDuty after 10 minutes
- Grouping: Batch multiple related failures into one incident
See our guide on API monitoring best practices.
4. Should I use WebSockets or Server-Sent Events?
WebSockets for:
- Bi-directional communication (clients can send commands)
- Very high-frequency updates (multiple per second)
- Custom binary protocols
Server-Sent Events (SSE) for:
- One-way updates (server → client)
- Simpler implementation
- Better compatibility with serverless (Vercel, Cloudflare)
- Automatic reconnection
For status dashboards, SSE is usually the better choice.
5. How do I store time-series data efficiently?
Use time-bucketing and aggregation:
-- Store raw checks for last 7 days
DELETE FROM checks WHERE checked_at < NOW() - INTERVAL '7 days';
-- Aggregate older data into hourly buckets
CREATE TABLE check_aggregates (
service_id TEXT,
hour TIMESTAMP,
avg_latency INT,
uptime_percentage DECIMAL,
total_checks INT
);
-- Run nightly:
INSERT INTO check_aggregates
SELECT
service_id,
date_trunc('hour', checked_at) as hour,
AVG(latency) as avg_latency,
(COUNT(*) FILTER (WHERE status = 'up')::FLOAT / COUNT(*)) * 100 as uptime_percentage,
COUNT(*) as total_checks
FROM checks
WHERE checked_at < NOW() - INTERVAL '7 days'
GROUP BY service_id, date_trunc('hour', checked_at);
Learn more in our PostgreSQL time-series optimization guide.
6. How do I monitor APIs behind authentication?
Store credentials securely and include them in health checks:
async function checkAuthenticatedEndpoint(url: string, apiKey: string) {
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'User-Agent': 'StatusDashboard/1.0',
},
});
// Validate response structure, not just status code
if (response.ok) {
const data = await response.json();
// Check for expected fields
if (!data.id || !data.status) {
throw new Error('Unexpected response structure');
}
}
return response;
}
Store API keys in environment variables or a secrets manager (AWS Secrets Manager, HashiCorp Vault).
7. Can I monitor GraphQL APIs?
Yes! Send a simple introspection or health check query:
async function checkGraphQLEndpoint(url: string) {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: '{ __typename }', // Minimal query
}),
});
const data = await response.json();
if (data.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(data.errors)}`);
}
return response;
}
For production, use a real query that exercises critical paths.
8. How do I calculate SLA compliance?
Calculate uptime percentage over a specific period:
async function calculateSLA(serviceId: string, days: number = 30) {
const since = new Date(Date.now() - days * 24 * 60 * 60 * 1000);
const checks = await prisma.check.findMany({
where: {
serviceId,
checkedAt: { gte: since },
},
});
const totalChecks = checks.length;
const upChecks = checks.filter(c => c.status === 'up').length;
const uptimePercentage = (upChecks / totalChecks) * 100;
// Common SLA tiers
const slaStatus =
uptimePercentage >= 99.99 ? 'Four Nines (99.99%)' :
uptimePercentage >= 99.95 ? 'Three Nines Plus (99.95%)' :
uptimePercentage >= 99.9 ? 'Three Nines (99.9%)' :
uptimePercentage >= 99.5 ? 'Two Nines Plus (99.5%)' :
'Below SLA';
const allowedDowntime = {
'99.99%': '4m 23s/month',
'99.95%': '21m 54s/month',
'99.9%': '43m 49s/month',
'99.5%': '3h 36m/month',
};
return {
uptimePercentage: uptimePercentage.toFixed(4),
slaStatus,
totalChecks,
period: `${days} days`,
};
}
Read more about understanding API SLAs.
9. What about monitoring SOAP APIs?
Parse SOAP XML responses:
import { parseStringPromise } from 'xml2js';
async function checkSOAPEndpoint(url: string, soapAction: string) {
const soapEnvelope = `
<?xml version="1.0"?>
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
<soap:Body>
<HealthCheck xmlns="http://yournamespace.com/" />
</soap:Body>
</soap:Envelope>
`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'text/xml',
'SOAPAction': soapAction,
},
body: soapEnvelope,
});
const xml = await response.text();
const parsed = await parseStringPromise(xml);
// Validate SOAP response structure
if (parsed['soap:Envelope']['soap:Body'][0]['soap:Fault']) {
throw new Error('SOAP Fault detected');
}
return response;
}
10. How do I handle APIs with dynamic responses?
Implement response validation:
async function checkWithValidation(url: string, validator: (data: any) => boolean) {
const response = await fetch(url);
const data = await response.json();
if (!validator(data)) {
throw new Error('Response validation failed');
}
return { status: 'up', data };
}
// Example validator
const stripeValidator = (data: any) => {
return data.object === 'balance' &&
typeof data.available === 'object' &&
Array.isArray(data.available);
};
await checkWithValidation('https://api.stripe.com/v1/balance', stripeValidator);
Wrapping Up
You now have a complete blueprint for building a production-ready API status dashboard. This tutorial covered:
✅ Architecture design and database schema
✅ Health check engine with incident tracking
✅ Real-time updates via Server-Sent Events
✅ Multi-channel alerting (email, Slack, webhooks)
✅ Deployment to Vercel, Railway, or self-hosted
✅ Best practices for time-series data and SLA tracking
Next steps:
- Clone the example repository
- Deploy to Vercel in under 10 minutes
- Add your first services to monitor
- Configure alerts for your team
Or skip the setup entirely and start using API Status Check — we've already built this for you.
Related guides:
- Best API Monitoring Tools in 2025
- How to Monitor Third-Party APIs
- Understanding API Rate Limits
- Railway API Status Guide
- Handling API Outages: Complete Guide
Have questions? Join our Discord community or email us.
Last updated: January 30, 2025
Monitor Your APIs
Check the real-time status of 100+ popular APIs used by developers.
View API Status →