Skip to main content
Back to Blog
Tutorials
4 min read
November 26, 2024

How to Build a Status and Uptime Page in Next.js

Create a public status page with service health indicators, incident history, and uptime percentages in Next.js.

Ryel Banfield

Founder & Lead Developer

A public status page builds trust with users. Here is how to create one with health checks and incident history.

Step 1: Data Types

// types/status.ts
export type ServiceStatus = "operational" | "degraded" | "partial_outage" | "major_outage" | "maintenance";

export interface Service {
  id: string;
  name: string;
  description: string;
  status: ServiceStatus;
  uptimePercentage: number;
  responseTime: number; // ms
  lastChecked: string;
}

export interface Incident {
  id: string;
  title: string;
  status: "investigating" | "identified" | "monitoring" | "resolved";
  severity: "minor" | "major" | "critical";
  createdAt: string;
  resolvedAt: string | null;
  updates: IncidentUpdate[];
  affectedServices: string[];
}

export interface IncidentUpdate {
  id: string;
  message: string;
  status: Incident["status"];
  createdAt: string;
}

export interface UptimeDay {
  date: string;
  status: ServiceStatus;
  uptimePercentage: number;
}

Step 2: Status Badge Component

import type { ServiceStatus } from "@/types/status";

const STATUS_CONFIG: Record<ServiceStatus, { label: string; color: string; dot: string }> = {
  operational: { label: "Operational", color: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400", dot: "bg-green-500" },
  degraded: { label: "Degraded Performance", color: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400", dot: "bg-yellow-500" },
  partial_outage: { label: "Partial Outage", color: "bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400", dot: "bg-orange-500" },
  major_outage: { label: "Major Outage", color: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400", dot: "bg-red-500" },
  maintenance: { label: "Maintenance", color: "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400", dot: "bg-blue-500" },
};

export function StatusBadge({ status }: { status: ServiceStatus }) {
  const config = STATUS_CONFIG[status];

  return (
    <span className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-0.5 text-xs font-medium ${config.color}`}>
      <span className={`h-1.5 w-1.5 rounded-full ${config.dot}`} />
      {config.label}
    </span>
  );
}

Step 3: Uptime Bar Visualization

import type { UptimeDay, ServiceStatus } from "@/types/status";

const DAY_COLORS: Record<ServiceStatus, string> = {
  operational: "bg-green-500",
  degraded: "bg-yellow-500",
  partial_outage: "bg-orange-500",
  major_outage: "bg-red-500",
  maintenance: "bg-blue-500",
};

interface UptimeBarProps {
  days: UptimeDay[];
  label: string;
}

export function UptimeBar({ days, label }: UptimeBarProps) {
  const avgUptime =
    days.reduce((sum, d) => sum + d.uptimePercentage, 0) / days.length;

  return (
    <div>
      <div className="mb-1 flex items-center justify-between">
        <span className="text-sm font-medium">{label}</span>
        <span className="text-sm text-gray-500">{avgUptime.toFixed(2)}% uptime</span>
      </div>
      <div className="flex gap-[2px]">
        {days.map((day) => (
          <div
            key={day.date}
            className={`h-8 flex-1 rounded-sm ${DAY_COLORS[day.status]} transition-all hover:opacity-80`}
            title={`${day.date}: ${day.uptimePercentage}%`}
          />
        ))}
      </div>
      <div className="mt-1 flex justify-between text-xs text-gray-400">
        <span>{days.length} days ago</span>
        <span>Today</span>
      </div>
    </div>
  );
}

Step 4: Service List Component

import type { Service } from "@/types/status";
import { StatusBadge } from "./StatusBadge";

export function ServiceList({ services }: { services: Service[] }) {
  const allOperational = services.every((s) => s.status === "operational");

  return (
    <div className="space-y-4">
      {/* Overall status banner */}
      <div
        className={`rounded-xl p-4 text-center text-sm font-medium ${
          allOperational
            ? "bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400"
            : "bg-yellow-50 text-yellow-700 dark:bg-yellow-900/20 dark:text-yellow-400"
        }`}
      >
        {allOperational
          ? "All systems operational"
          : "Some systems are experiencing issues"}
      </div>

      {/* Service rows */}
      <div className="divide-y rounded-xl border dark:border-gray-700 dark:divide-gray-700">
        {services.map((service) => (
          <div
            key={service.id}
            className="flex items-center justify-between px-4 py-3"
          >
            <div>
              <p className="text-sm font-medium">{service.name}</p>
              <p className="text-xs text-gray-400">{service.description}</p>
            </div>
            <div className="flex items-center gap-3">
              <span className="text-xs text-gray-400">
                {service.responseTime}ms
              </span>
              <StatusBadge status={service.status} />
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

Step 5: Incident Timeline

import type { Incident } from "@/types/status";

const SEVERITY_COLORS = {
  minor: "border-yellow-500",
  major: "border-orange-500",
  critical: "border-red-500",
};

const STATUS_LABELS = {
  investigating: "Investigating",
  identified: "Identified",
  monitoring: "Monitoring",
  resolved: "Resolved",
};

export function IncidentTimeline({ incidents }: { incidents: Incident[] }) {
  if (incidents.length === 0) {
    return (
      <p className="rounded-xl border p-6 text-center text-sm text-gray-400 dark:border-gray-700">
        No incidents reported in the last 90 days
      </p>
    );
  }

  return (
    <div className="space-y-4">
      {incidents.map((incident) => (
        <div
          key={incident.id}
          className={`rounded-xl border-l-4 bg-white p-4 shadow-sm dark:bg-gray-900 ${SEVERITY_COLORS[incident.severity]}`}
        >
          <div className="flex items-start justify-between">
            <h3 className="text-sm font-semibold">{incident.title}</h3>
            <span
              className={`rounded-full px-2 py-0.5 text-xs font-medium ${
                incident.status === "resolved"
                  ? "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400"
                  : "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
              }`}
            >
              {STATUS_LABELS[incident.status]}
            </span>
          </div>

          <div className="mt-3 space-y-2 border-l-2 border-gray-200 pl-4 dark:border-gray-700">
            {incident.updates.map((update) => (
              <div key={update.id}>
                <p className="text-xs font-medium text-gray-500">
                  {STATUS_LABELS[update.status]} —{" "}
                  {new Date(update.createdAt).toLocaleString()}
                </p>
                <p className="text-sm text-gray-600 dark:text-gray-400">
                  {update.message}
                </p>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
}

Step 6: Status Page Route

// app/status/page.tsx
import { ServiceList } from "@/components/status/ServiceList";
import { UptimeBar } from "@/components/status/UptimeBar";
import { IncidentTimeline } from "@/components/status/IncidentTimeline";

async function getStatusData() {
  // Fetch from your monitoring API
  const res = await fetch(`${process.env.API_URL}/status`, {
    next: { revalidate: 60 },
  });
  return res.json();
}

export default async function StatusPage() {
  const { services, uptimeHistory, incidents } = await getStatusData();

  return (
    <div className="mx-auto max-w-3xl px-4 py-12">
      <h1 className="mb-8 text-2xl font-bold">System Status</h1>

      <ServiceList services={services} />

      <section className="mt-12 space-y-6">
        <h2 className="text-lg font-semibold">Uptime (90 days)</h2>
        {Object.entries(uptimeHistory).map(([serviceId, days]) => {
          const service = services.find((s: any) => s.id === serviceId);
          return (
            <UptimeBar
              key={serviceId}
              days={days as any}
              label={service?.name ?? serviceId}
            />
          );
        })}
      </section>

      <section className="mt-12">
        <h2 className="mb-4 text-lg font-semibold">Recent Incidents</h2>
        <IncidentTimeline incidents={incidents} />
      </section>
    </div>
  );
}

Step 7: Health Check API

// app/api/health/route.ts
import { NextResponse } from "next/server";

export async function GET() {
  const checks = await Promise.allSettled([
    checkDatabase(),
    checkRedis(),
    checkExternalAPI(),
  ]);

  const results = {
    database: checks[0].status === "fulfilled" ? "healthy" : "unhealthy",
    redis: checks[1].status === "fulfilled" ? "healthy" : "unhealthy",
    api: checks[2].status === "fulfilled" ? "healthy" : "unhealthy",
  };

  const allHealthy = Object.values(results).every((v) => v === "healthy");

  return NextResponse.json(
    { status: allHealthy ? "healthy" : "degraded", checks: results },
    { status: allHealthy ? 200 : 503 }
  );
}

async function checkDatabase() {
  // Run a simple query
  const start = Date.now();
  // await db.execute(sql`SELECT 1`);
  return { responseTime: Date.now() - start };
}

async function checkRedis() {
  const start = Date.now();
  // await redis.ping();
  return { responseTime: Date.now() - start };
}

async function checkExternalAPI() {
  const start = Date.now();
  const res = await fetch("https://api.example.com/health");
  if (!res.ok) throw new Error("API unhealthy");
  return { responseTime: Date.now() - start };
}

Summary

  • Visual uptime bars show 90-day history at a glance
  • Service-level status with response times
  • Incident timeline with update history
  • Health check API for automated monitoring
  • Revalidate every 60 seconds for near-real-time data

Need Monitoring Solutions?

We build custom monitoring and status page systems for SaaS products. Contact us to discuss your project.

status pageuptimemonitoringNext.jstutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles