tutorial

Monitoring Your Nuxt.js Application with Vigilmon

Add production-grade monitoring to Nuxt 3 — server-side health endpoints, SSR uptime checks, scheduled heartbeats, and webhook alerts for Vue/Nuxt apps.

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/health server 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

Email

Configure under Vigilmon → Alert Channels → Email. Alerts fire when:

  • /api/health returns non-2xx
  • 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 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

  1. Break the DB: set an invalid database URL and restart the dev server — /api/health should return 503. Vigilmon should alert within 60 seconds.
  2. Keyword check: temporarily change the homepage title so the keyword is missing — the SSR monitor should alert.
  3. Stop the heartbeat: comment out the Vigilmon ping in your task and deploy — after the heartbeat window expires, you should get an alert.
  4. Trigger a Vue error: throw inside a component and confirm the webhook fires and appears in your Slack channel.
  5. Recover: fix each issue and confirm Vigilmon sends "back online" notifications.

Production Checklist

  • [ ] /api/health has Cache-Control: no-store
  • [ ] Both the API route and SSR homepage are monitored separately
  • [ ] VIGILMON_HEARTBEAT_URL is set in your deployment environment
  • [ ] Nitro scheduled tasks are enabled and the task deploys correctly
  • [ ] VIGILMON_WEBHOOK_URL is 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/health every 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!

Monitor your app with Vigilmon

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

Start free →