tutorial

Monitoring Your Next.js App with Vigilmon: API Routes, Cron Jobs & Uptime

Add production-grade monitoring to Next.js — health API routes, Vercel cron heartbeats, error boundary webhook alerts, and uptime checks for SSR and static pages.

Next.js apps fail in subtle ways. Your server-rendered pages might be returning empty HTML due to a crashed data-fetch. Your Vercel edge deployment might have stale environment variables. Your /api routes might be timing out under load while your homepage looks fine. Vigilmon surfaces these issues before users notice. This tutorial shows you how to wire a Next.js app into Vigilmon for uptime monitoring, cron-based heartbeats, and alert routing.

What You'll Build

  • A /api/health route returning 200 with a database check
  • Vigilmon HTTP monitors for SSR pages and API routes
  • A Vercel cron heartbeat via /api/heartbeat
  • An error boundary that fires a webhook alert on render errors
  • Email and Slack alert channels

Prerequisites

  • A Next.js 14+ project (App Router or Pages Router both work)
  • A free Vigilmon account
  • Optionally: a database (Postgres via Prisma, PlanetScale, etc.)

Step 1: Create the Health API Route

App Router (app/api/health/route.ts)

import { NextResponse } from "next/server";
import { db } from "@/lib/db"; // your database client

export const dynamic = "force-dynamic"; // never cache this route

export async function GET() {
  const checks: Record<string, string> = {};
  let status = "ok";

  try {
    // Replace with your actual DB check
    await db.$queryRaw`SELECT 1`;
    checks.database = "ok";
  } catch (err) {
    checks.database = `error: ${(err as Error).message}`;
    status = "degraded";
  }

  const httpStatus = status === "ok" ? 200 : 503;

  return NextResponse.json(
    {
      status,
      timestamp: new Date().toISOString(),
      checks,
    },
    { status: httpStatus }
  );
}

Pages Router (pages/api/health.ts)

import type { NextApiRequest, NextApiResponse } from "next";
import { db } from "@/lib/db";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const checks: Record<string, string> = {};
  let status = "ok";

  try {
    await db.$queryRaw`SELECT 1`;
    checks.database = "ok";
  } catch (err) {
    checks.database = `error: ${(err as Error).message}`;
    status = "degraded";
  }

  res.setHeader("Cache-Control", "no-store");
  res.status(status === "ok" ? 200 : 503).json({
    status,
    timestamp: new Date().toISOString(),
    checks,
  });
}

Verify it locally:

curl -s http://localhost:3000/api/health | jq
# {
#   "status": "ok",
#   "timestamp": "2025-06-29T10:00:00.000Z",
#   "checks": { "database": "ok" }
# }

The Cache-Control: no-store header is important — without it, a CDN or Vercel edge cache might return a stale 200 even after your database connection drops.


Step 2: Create Vigilmon HTTP Monitors

Log in to Vigilmon and create two monitors.

Monitor 1: API Health Endpoint

| Field | Value | |---|---| | URL | https://yourapp.vercel.app/api/health | | Method | GET | | Check interval | 60 seconds | | Expected status | 200 | | Timeout | 10 seconds |

Monitor 2: SSR Homepage

| Field | Value | |---|---| | URL | https://yourapp.vercel.app | | Method | GET | | Check interval | 5 minutes | | Expected status | 200 | | Keyword check | A string that only appears when the page renders correctly, e.g. your app name |

The keyword check catches the case where your page returns 200 with empty or error HTML — something a pure HTTP status check would miss.

Static export note: If you run next export, your pages are served as static HTML from a CDN. Point the Vigilmon monitor at the CDN URL directly (e.g. your Cloudflare Pages or S3 URL). There's no server to check database connectivity on — monitor your data API separately.


Step 3: Vercel Cron Heartbeat

Vercel Cron Jobs (available on Hobby and Pro plans) let you run a function on a schedule without an external scheduler. We'll use a /api/heartbeat route that does meaningful work and pings Vigilmon on success.

Create the Heartbeat Route

App Router (app/api/heartbeat/route.ts):

import { NextRequest, NextResponse } from "next/server";

// Protect with a shared secret so only Vercel can trigger this
function isAuthorized(req: NextRequest): boolean {
  const auth = req.headers.get("authorization");
  return auth === `Bearer ${process.env.CRON_SECRET}`;
}

export async function GET(req: NextRequest) {
  if (!isAuthorized(req)) {
    return NextResponse.json({ error: "unauthorized" }, { status: 401 });
  }

  try {
    // Do your actual scheduled work here
    // e.g. await sendScheduledEmails();
    //      await refreshExternalCache();

    // Ping Vigilmon to confirm the job ran successfully
    await fetch(process.env.VIGILMON_HEARTBEAT_URL!, {
      method: "GET",
      signal: AbortSignal.timeout(5000),
    });

    return NextResponse.json({ ok: true, ts: new Date().toISOString() });
  } catch (err) {
    console.error("[heartbeat] failed:", err);
    // Don't ping Vigilmon — the silence triggers the alert
    return NextResponse.json(
      { error: "heartbeat job failed" },
      { status: 500 }
    );
  }
}

Configure Vercel Cron

Create vercel.json in your project root (or add to an existing one):

{
  "crons": [
    {
      "path": "/api/heartbeat",
      "schedule": "*/5 * * * *"
    }
  ]
}

This runs the heartbeat every 5 minutes. Vercel automatically injects an Authorization: Bearer <CRON_SECRET> header — set CRON_SECRET in your Vercel environment variables, and add the same value to your local .env.local for testing.

Set your Vigilmon heartbeat URL:

VIGILMON_HEARTBEAT_URL=https://vigilmon.online/api/heartbeats/YOUR-UUID/ping
CRON_SECRET=your-random-secret-here

Vigilmon's heartbeat monitor will alert you if the ping stops arriving — catching Vercel cron failures, quota exhaustion, or function timeouts.


Step 4: Error Boundary with Webhook Alert

React's error boundaries catch rendering errors that would otherwise show a blank screen. By adding a webhook call inside componentDidCatch, you get instant alerts when users hit a crash.

// components/ErrorBoundary.tsx
"use client"; // Required for App Router

import React, { Component, ErrorInfo, ReactNode } from "react";

interface Props {
  children: ReactNode;
  fallback?: ReactNode;
}

interface State {
  hasError: boolean;
  errorId?: string;
}

export class ErrorBoundary extends Component<Props, State> {
  constructor(props: Props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(): State {
    return { hasError: true };
  }

  async componentDidCatch(error: Error, info: ErrorInfo) {
    const webhookUrl = process.env.NEXT_PUBLIC_VIGILMON_WEBHOOK_URL;
    if (!webhookUrl) return;

    try {
      await fetch(webhookUrl, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          message: `React render error: ${error.message}`,
          stack: error.stack?.slice(0, 500),
          componentStack: info.componentStack?.slice(0, 300),
          url: window.location.href,
          timestamp: new Date().toISOString(),
        }),
      });
    } catch {
      // Silently fail — the UI is already broken, don't compound it
    }
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div className="error-fallback">
          <h2>Something went wrong.</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

Wrap your root layout (App Router) or _app.tsx (Pages Router):

// app/layout.tsx (App Router)
import { ErrorBoundary } from "@/components/ErrorBoundary";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <ErrorBoundary
          fallback={<p>An unexpected error occurred. Please refresh.</p>}
        >
          {children}
        </ErrorBoundary>
      </body>
    </html>
  );
}

Get the Webhook URL from Vigilmon → Alert Channels → Add Channel → Webhook, and set it in your Vercel environment variables:

NEXT_PUBLIC_VIGILMON_WEBHOOK_URL=https://vigilmon.online/api/webhooks/YOUR-UUID

Note on NEXT_PUBLIC_: This prefix exposes the variable in client-side code. Vigilmon webhook URLs don't contain secrets — they're safe to expose. If you're concerned, proxy the call through your own /api/report-error route and call Vigilmon from the server side.


Step 5: Alert Routing

In Vigilmon, configure two alert channels:

Email

Set up under Alert Channels → Email. You'll receive alerts when:

  • /api/health returns non-200
  • The SSR homepage keyword check fails
  • The heartbeat window expires

Slack Webhook

  1. Create a Slack incoming webhook for your team channel
  2. Vigilmon → Alert Channels → Add Channel → Webhook → paste the Slack URL
  3. Assign the channel to all three monitors

Example Slack alert:

🔴 *yourapp.vercel.app/api/health* is DOWN
Status: 503 | Check: database error: Connection refused
Duration: 3m 42s

Step 6: Test the Full Loop

  1. Break the DB: disconnect your database env var and redeploy — /api/health should return 503, and Vigilmon should alert within 60 seconds.
  2. Trigger a render error: throw intentionally in a component wrapped by ErrorBoundary — confirm the webhook fires.
  3. Pause the cron: comment out the heartbeat ping in route.ts and deploy — Vigilmon should alert after the heartbeat window expires (default 90 seconds after the last ping).
  4. Recover and verify: fix each issue and confirm Vigilmon sends "back online" notifications.

Production Checklist

  • [ ] /api/health uses no-store cache control
  • [ ] Both API route and SSR page are monitored separately
  • [ ] CRON_SECRET and VIGILMON_HEARTBEAT_URL are set in Vercel environment variables
  • [ ] Error boundary wraps the root layout
  • [ ] Alert channels tested end-to-end
  • [ ] Maintenance windows configured for Vercel preview deployments

Wrapping Up

Your Next.js app now has layered monitoring:

  • Uptime: Vigilmon polls /api/health and your homepage every minute
  • Heartbeat: Vercel cron confirms your scheduled jobs are running
  • Render errors: Error boundary fires a webhook when users hit a crash

All three layers together mean you'll know about a production issue before a user files a support ticket.

Sign up for Vigilmon — free tier includes multiple monitors with no credit card required.

Questions or edge cases? Drop them in the comments below!

Monitor your app with Vigilmon

Free plan — 5 monitors, no credit card required. Up and running in 60 seconds.

Start free →