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/healthroute 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:
Set up under Alert Channels → Email. You'll receive alerts when:
/api/healthreturns non-200- The SSR homepage keyword check fails
- The heartbeat window expires
Slack Webhook
- Create a Slack incoming webhook for your team channel
- Vigilmon → Alert Channels → Add Channel → Webhook → paste the Slack URL
- 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
- Break the DB: disconnect your database env var and redeploy —
/api/healthshould return503, and Vigilmon should alert within 60 seconds. - Trigger a render error: throw intentionally in a component wrapped by
ErrorBoundary— confirm the webhook fires. - Pause the cron: comment out the heartbeat ping in
route.tsand deploy — Vigilmon should alert after the heartbeat window expires (default 90 seconds after the last ping). - Recover and verify: fix each issue and confirm Vigilmon sends "back online" notifications.
Production Checklist
- [ ]
/api/healthusesno-storecache control - [ ] Both API route and SSR page are monitored separately
- [ ]
CRON_SECRETandVIGILMON_HEARTBEAT_URLare 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/healthand 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!