Nuxt 3 apps run on Nitro, which means your server-side routes and API handlers can fail silently while your static pages look fine. A database connection pool exhaustion won't break the homepage — but it will break every authenticated page and API call. Vigilmon catches this class of failure before your users do. This tutorial walks through adding health endpoints, HTTP monitors, heartbeat checks, and alert routing to a Nuxt 3 application.
What You'll Build
- A
/api/healthserver route with DB and cache checks - Vigilmon HTTP monitors for API routes and SSR pages
- A scheduled Nitro task that pings a heartbeat URL
- Webhook alerts on errors
- Email and Slack notification channels
Prerequisites
- A Nuxt 3 project
- A free Vigilmon account
- Optionally: a database (Prisma, Drizzle, or any ORM) and Redis
Step 1: Create the Health Server Route
Nuxt 3's server directory gives you filesystem-based API routes. Create a dedicated health endpoint that checks your critical dependencies.
// server/routes/api/health.get.ts
export default defineEventHandler(async (event) => {
const checks: Record<string, string> = {};
let overallStatus = "ok";
// --- Database check ---
try {
// Replace with your actual DB client call
// e.g. await db.execute(sql`SELECT 1`)
// For demonstration we assume a global `db` is available
// @ts-ignore
if (globalThis.__db) {
// @ts-ignore
await globalThis.__db.execute("SELECT 1");
}
checks.database = "ok";
} catch (err: any) {
checks.database = `error: ${err?.message ?? "unknown"}`;
overallStatus = "degraded";
}
// --- Cache / Redis check ---
try {
// Nuxt's useStorage() is available in server context
const storage = useStorage("redis"); // or "memory", "fs"
await storage.setItem("__health_ping", "1", { ttl: 10 });
const val = await storage.getItem("__health_ping");
checks.cache = val === "1" ? "ok" : "miss";
if (checks.cache !== "ok") overallStatus = "degraded";
} catch (err: any) {
checks.cache = `error: ${err?.message ?? "unknown"}`;
overallStatus = "degraded";
}
const httpStatus = overallStatus === "ok" ? 200 : 503;
setResponseStatus(event, httpStatus);
setHeader(event, "Cache-Control", "no-store");
return {
status: overallStatus,
timestamp: new Date().toISOString(),
checks,
};
});
Test it locally:
curl -s http://localhost:3000/api/health | jq
# {
# "status": "ok",
# "timestamp": "2025-06-29T10:00:00.000Z",
# "checks": {
# "database": "ok",
# "cache": "ok"
# }
# }
The Cache-Control: no-store header prevents Nitro's built-in response cache and any upstream CDN from serving a stale 200 after a dependency fails.
No-database variant: If your Nuxt app is purely static or uses a third-party API, simplify the health check:
// server/routes/api/health.get.ts
export default defineEventHandler((event) => {
setHeader(event, "Cache-Control", "no-store");
return { status: "ok", timestamp: new Date().toISOString() };
});
Even a simple 200 lets Vigilmon confirm your Nitro server is up and responding.
Step 2: Create Vigilmon HTTP Monitors
Log in to Vigilmon and create two monitors.
Monitor 1: API Health Endpoint
| Field | Value |
|---|---|
| URL | https://yourapp.com/api/health |
| Method | GET |
| Check interval | 60 seconds |
| Expected status | 200 |
| Timeout | 10 seconds |
Monitor 2: SSR Homepage
| Field | Value |
|---|---|
| URL | https://yourapp.com |
| Method | GET |
| Check interval | 5 minutes |
| Expected status | 200 |
| Keyword check | A string that only appears in a correctly rendered page (your app name, a nav item, etc.) |
The keyword check matters because Nuxt's SSR can return 200 with a skeleton HTML shell or error page when the server-side data fetch fails. Pure HTTP status monitoring misses this — the keyword check doesn't.
Static generation note: If you run nuxt generate, your pages are pre-rendered HTML files served from a CDN. Point Vigilmon at the CDN URL. Add a separate monitor for any runtime API your generated pages call.
Step 3: Scheduled Heartbeat with Nitro Tasks
Nuxt 3.9+ includes Nitro Scheduled Tasks — a built-in way to run periodic server-side code without an external scheduler.
Enable scheduled tasks in nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
experimental: {
tasks: true,
},
scheduledTasks: {
// Run every 5 minutes (cron expression)
"0/5 * * * *": ["app:heartbeat"],
},
},
});
Create the task:
// server/tasks/app/heartbeat.ts
export default defineTask({
meta: {
name: "app:heartbeat",
description: "Confirm scheduled work is running and ping Vigilmon",
},
async run({ payload, context }) {
// Do your actual scheduled work here
// e.g. await sendPendingNotifications()
// await refreshExternalCache()
console.log("[heartbeat] scheduled task starting");
const heartbeatUrl = process.env.VIGILMON_HEARTBEAT_URL;
if (!heartbeatUrl) {
console.warn("[heartbeat] VIGILMON_HEARTBEAT_URL not set, skipping ping");
return { result: "ok_no_ping" };
}
try {
const res = await $fetch(heartbeatUrl, { method: "GET" });
console.log("[heartbeat] pinged Vigilmon");
} catch (err) {
// Work succeeded but ping failed — still log the error
console.error("[heartbeat] Vigilmon ping failed:", err);
}
return { result: "ok" };
},
});
Set the environment variable:
VIGILMON_HEARTBEAT_URL=https://vigilmon.online/api/heartbeats/YOUR-UUID/ping
Get your heartbeat URL from Vigilmon → Dashboard → Heartbeat Monitors → New. Set the expected window to 6 minutes (one interval plus a grace period).
Vigilmon alerts you if the ping stops arriving — catching Nitro task failures, uncaught exceptions inside the task, or deployment issues that stop scheduled execution entirely.
Manual trigger for testing: Nitro exposes a /_nitro/tasks endpoint in development that lets you trigger tasks manually:
curl -X POST http://localhost:3000/_nitro/tasks/app:heartbeat
Step 4: Vue Error Handler with Webhook Alerts
Nuxt's vueApp.config.errorHandler captures unhandled errors from Vue components. Wire it to a Vigilmon webhook to get instant notifications when users hit a crash.
Create a plugin:
// plugins/error-handler.client.ts
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.config.errorHandler = async (err, instance, info) => {
console.error("[errorHandler]", err, info);
const webhookUrl = useRuntimeConfig().public.vigilmonWebhookUrl;
if (!webhookUrl) return;
try {
await $fetch(webhookUrl, {
method: "POST",
body: {
message: `Vue error: ${(err as Error)?.message ?? String(err)}`,
info,
url: window.location.href,
timestamp: new Date().toISOString(),
},
});
} catch {
// Silently fail — don't compound the existing error
}
};
});
Add the webhook URL to nuxt.config.ts runtime config:
// nuxt.config.ts
export default defineNuxtConfig({
runtimeConfig: {
public: {
vigilmonWebhookUrl: process.env.VIGILMON_WEBHOOK_URL ?? "",
},
},
});
VIGILMON_WEBHOOK_URL=https://vigilmon.online/api/webhooks/YOUR-UUID
Get the webhook URL from Vigilmon → Alert Channels → Add Channel → Webhook.
Step 5: Alert Routing
Configure under Vigilmon → Alert Channels → Email. Alerts fire when:
/api/healthreturns non-2xx- 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 Slack channel to all your monitors and the heartbeat
Example Slack alert:
🔴 *yourapp.com/api/health* is DOWN
Status: 503 | checks.database: error: Connection refused
Duration: 2m 18s
Step 6: Test the Full Loop
- Break the DB: set an invalid database URL and restart the dev server —
/api/healthshould return503. Vigilmon should alert within 60 seconds. - Keyword check: temporarily change the homepage title so the keyword is missing — the SSR monitor should alert.
- Stop the heartbeat: comment out the Vigilmon ping in your task and deploy — after the heartbeat window expires, you should get an alert.
- Trigger a Vue error: throw inside a component and confirm the webhook fires and appears in your Slack channel.
- Recover: fix each issue and confirm Vigilmon sends "back online" notifications.
Production Checklist
- [ ]
/api/healthhasCache-Control: no-store - [ ] Both the API route and SSR homepage are monitored separately
- [ ]
VIGILMON_HEARTBEAT_URLis set in your deployment environment - [ ] Nitro scheduled tasks are enabled and the task deploys correctly
- [ ]
VIGILMON_WEBHOOK_URLis set for Vue error alerts - [ ] All alert channels tested end-to-end
- [ ] Maintenance windows configured for zero-downtime deployments
Wrapping Up
Your Nuxt 3 app now has layered monitoring:
- Uptime: Vigilmon polls
/api/healthevery 60 seconds, checking the database and cache - SSR accuracy: a keyword check confirms the page renders real content, not an error shell
- Heartbeat: Nitro scheduled tasks confirm background jobs are running
- Render errors: the Vue error handler fires a webhook when users hit a crash
Together these layers mean you'll know about a production issue before a user reports it — and the specific check that failed tells you exactly where to look.
Sign up for Vigilmon — free tier includes multiple monitors, no credit card required.
Questions or Nuxt-specific edge cases? Drop them in the comments!