Nitro is the server engine that powers Nuxt 3, Analog, and a growing list of full-stack frameworks. It runs on Node.js, Deno, Bun, and edge runtimes — but regardless of where it runs, it can go down. A misconfigured environment variable, a failed cold start, or a database connection failure will return errors to your users long before anyone notices. Vigilmon catches those failures from the outside and alerts you the moment they happen.
This tutorial adds production-grade monitoring to a standalone Nitro application.
What You'll Build
- A
/healthAPI route in your Nitro app that checks real dependencies - A Vigilmon HTTP monitor watching your health endpoint
- A heartbeat monitor for any Nitro scheduled tasks
- Alert channels (email + Slack)
Prerequisites
- A Nitro project (standalone, or embedded in Nuxt/Analog)
- Node.js 18+ or Bun
- A free account at vigilmon.online
Step 1: Add a Health Check Route
Nitro uses file-based routing. Create server/routes/health.get.ts (or .js for JavaScript projects):
// server/routes/health.get.ts
import { defineEventHandler, setResponseStatus } from "h3";
interface HealthCheck {
name: string;
status: "ok" | "error";
detail?: string;
}
export default defineEventHandler(async (event) => {
const checks: HealthCheck[] = [];
let allOk = true;
// 1. Database probe — replace with your actual DB client
try {
// Example for a Postgres client available via useStorage or nitropack context
// await db.query("SELECT 1");
checks.push({ name: "database", status: "ok" });
} catch (e) {
checks.push({
name: "database",
status: "error",
detail: (e as Error).message,
});
allOk = false;
}
// 2. External API probe — replace with your real dependency
try {
const resp = await $fetch<unknown>("https://api.example.com/ping", {
timeout: 3000,
}).catch((err) => {
throw err;
});
checks.push({ name: "upstream_api", status: "ok" });
} catch (e) {
checks.push({
name: "upstream_api",
status: "error",
detail: (e as Error).message,
});
allOk = false;
}
// 3. Storage probe — check Nitro's built-in storage (if used)
try {
const storage = useStorage();
await storage.setItem("health:probe", "1");
const val = await storage.getItem("health:probe");
const storageOk = val === "1";
checks.push({
name: "storage",
status: storageOk ? "ok" : "error",
detail: storageOk ? undefined : "read-after-write mismatch",
});
if (!storageOk) allOk = false;
} catch (e) {
checks.push({
name: "storage",
status: "error",
detail: (e as Error).message,
});
allOk = false;
}
if (!allOk) {
setResponseStatus(event, 503);
}
return {
status: allOk ? "ok" : "degraded",
checks,
ts: new Date().toISOString(),
};
});
Start your dev server and verify:
npx nitro dev
curl http://localhost:3000/health
Expected response:
{
"status": "ok",
"checks": [
{ "name": "database", "status": "ok" },
{ "name": "upstream_api", "status": "ok" },
{ "name": "storage", "status": "ok" }
],
"ts": "2026-01-15T10:23:00.000Z"
}
When any check fails, the route returns 503 with the failing check's detail field populated. That's exactly what you want — a monitor that knows what broke, not just that something did.
Tip: If you only use Nitro as the runtime for Nuxt or Analog, the same file path (
server/routes/health.get.ts) works there too. Nuxt serves it at/healthautomatically.
Step 2: Set Up the Vigilmon HTTP Monitor
With the health endpoint deployed to your server or hosting platform:
- Log in to Vigilmon and click New Monitor → HTTP.
- Set URL to
https://yourapp.example.com/health. - Set Check interval to 60 seconds.
- Set Expected status code to
200. - Under Advanced → JSON body assertion:
- Path:
status - Expected value:
ok
- Path:
- Save.
Vigilmon polls your endpoint from multiple external regions. If it returns a non-200 or status != "ok", you get alerted within the check interval.
You can add extra monitors for other critical routes:
https://yourapp.example.com/api/ping → lightweight alive check
https://yourapp.example.com/health → full dependency check
https://yourapp.example.com/api/v1/status → API versioning alive
Each monitor is independent, so a failure on one doesn't mask a failure on another.
Step 3: Alert Channels
In Vigilmon, go to Notifications → New Channel:
- Email — on-call address for immediate paging
- Slack webhook — post to your team channel
When your Nitro app goes down:
🔴 DOWN: yourapp.example.com/health
Status: 503 Service Unavailable
Region: AP-Southeast
2 minutes ago
When it recovers:
✅ RECOVERED: yourapp.example.com/health
Downtime: 4 minutes
Step 4: Heartbeat Monitoring for Nitro Scheduled Tasks
Nitro supports scheduled tasks via nitro.config.ts. If a scheduled task stops running silently — crashes, hangs, or simply isn't triggered by the host — your HTTP health endpoint stays green while work stops happening.
The heartbeat pattern: ping a unique Vigilmon URL at the end of each successful run. No ping within the grace period → alert.
Configure a task in nitro.config.ts:
// nitro.config.ts
export default defineNitroConfig({
experimental: {
tasks: true,
},
});
Create server/tasks/cleanup.ts:
// server/tasks/cleanup.ts
export default defineTask({
meta: {
name: "cleanup",
description: "Nightly database cleanup",
},
async run({ payload, context }) {
await performCleanup();
// Ping Vigilmon heartbeat after successful run
const heartbeatUrl = process.env.VIGILMON_CLEANUP_HEARTBEAT;
if (heartbeatUrl) {
await $fetch(heartbeatUrl, { method: "POST" }).catch(() => {
// Log but don't rethrow — heartbeat failure shouldn't fail the task
console.error("Failed to ping Vigilmon heartbeat");
});
}
return { result: "success" };
},
});
async function performCleanup() {
// your cleanup logic here
}
Set the environment variable:
VIGILMON_CLEANUP_HEARTBEAT=https://vigilmon.online/api/heartbeat/<your-id>
In Vigilmon, create a Heartbeat Monitor:
- Click New Monitor → Heartbeat
- Set expected interval to match your task schedule (e.g., 25 hours for a nightly job)
- Copy the ping URL into your environment config
If the task crashes or its host scheduler stops triggering it, Vigilmon alerts you after one missed interval.
Step 5: Protecting the Health Endpoint in Production
If your Nitro app sits behind authentication middleware, the /health route needs to bypass it. Add a middleware exception in server/middleware/auth.ts:
// server/middleware/auth.ts
export default defineEventHandler((event) => {
// Skip auth for the health check
if (event.path === "/health") return;
// ... your auth logic
});
Alternatively, protect the health endpoint itself with a secret token that you configure in Vigilmon as a custom request header:
// server/routes/health.get.ts (protected variant)
export default defineEventHandler(async (event) => {
const token = getHeader(event, "x-health-token");
if (token !== process.env.HEALTH_CHECK_TOKEN) {
setResponseStatus(event, 401);
return { error: "unauthorized" };
}
// ... rest of health check
});
In Vigilmon, add a Custom header to the monitor: x-health-token: <your-secret>.
Step 6: Status Page
Go to Status Pages → New Status Page in Vigilmon. Add your monitors, set a name, and share the public URL. Embed the badge in your project README:

What You've Built
| What | How |
|---|---|
| Health endpoint | Nitro file-based route at /health |
| Dependency checks | Database, upstream API, Nitro storage |
| HTTP uptime monitoring | Vigilmon HTTP monitor + JSON body assertion |
| Alert channels | Email + Slack via Vigilmon notification channels |
| Scheduled task monitoring | Heartbeat ping in Nitro task handler |
| Status page | Vigilmon public status page + badge |
Nitro's flexibility is its strength — it runs everywhere. That same flexibility means there's no single place to see whether all your deployments are healthy. Vigilmon fills that gap with an external monitor that doesn't care what runtime you're on.
Monitor your Nitro app today — register free at vigilmon.online.