DEV Community

Roberto Luna
Roberto Luna

Posted on

Adding a diagnostic WhatsApp status endpoint to a Next.js API (`app/api/whatsapp/estado/route.ts`)

Adding a diagnostic WhatsApp status endpoint to a Next.js API (app/api/whatsapp/estado/route.ts)

TL;DR: I added a new admin‑only endpoint (/api/whatsapp/estado) that returns the health of our WhatsApp Business API client and automatically subscribes the webhook if needed. The change also fixed a routing conflict that was causing every /api/whatsapp/* request to return 404.


The Problem

Our ticketing platform albaida‑tickets relies on a WhatsApp Business API client to receive voice notes from residents. The client runs in production behind a webhook that forwards incoming messages to our backend. After a weekend of silent tickets, the admin console showed an empty “WhatsApp status” widget, and the logs only contained:

Error: GET https://graph.facebook.com/v17.0/<WHATSAPP_ID>/messages?access_token=***
          ^^^
          404 Not Found
Enter fullscreen mode Exit fullscreen mode

Two things were wrong:

  1. The admin UI had no way to query the WhatsApp client health.
  2. All API routes under pages/api/whatsapp/* were returning 404, even though the files existed.

The root cause was a routing bug introduced when we mixed the Pages Router (pages/api/...) with the App Router (app/api/...) in a Next.js 13 project. The Pages Router took precedence and swallowed any request that matched the folder, but we had no index.ts there, so the request fell through to a 404.


What I Tried First

My first attempt was to add a quick‑and‑dirty file under the old Pages Router:

pages/api/whatsapp/status.ts
Enter fullscreen mode Exit fullscreen mode
// pages/api/whatsapp/status.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { getWhatsAppHealth } from '../../../lib/whatsapp';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  if (req.method !== 'GET') return res.status(405).end();
  try {
    const health = await getWhatsAppHealth();
    res.status(200).json(health);
  } catch (e) {
    res.status(500).json({ error: (e as Error).message });
  }
}
Enter fullscreen mode Exit fullscreen mode

I hit the endpoint (/api/whatsapp/status) and got a 404. The file was correctly placed, but the request never reached it. I inspected the Next.js routing table and realized that any folder named whatsapp under pages/api was being shadowed by a catch‑all dynamic route we had in pages/api/[...slug].ts for legacy integrations. The catch‑all was evaluated first, returning a 404 for unknown slugs.

I tried moving the catch‑all to a lower priority (pages/api/legacy/[...slug].ts) and adding a custom rewrites entry in next.config.js, but that introduced a new set of edge‑case bugs where other third‑party integrations started failing.


The Implementation

The clean solution was to switch the new endpoint to the App Router, which isolates it from the legacy Pages Router and gives us built‑in support for route handlers, middlewares, and dynamic metadata.

1. File structure

app/
 └─ api/
     └─ whatsapp/
         └─ estado/
             └─ route.ts   ← new endpoint
Enter fullscreen mode Exit fullscreen mode

2. The route handler (app/api/whatsapp/estado/route.ts)

// app/api/whatsapp/estado/route.ts
import { NextResponse } from 'next/server';
import { getWhatsAppHealth, ensureWebhook } from '@/lib/whatsapp';
import { adminAuth } from '@/middleware/adminAuth';

// Force dynamic rendering – we always want fresh data
export const dynamic = 'force-dynamic';

// Only allow GET requests
export async function GET(request: Request) {
  // 1️⃣ Authenticate the admin user
  const authResult = await adminAuth(request);
  if (!authResult.ok) {
    return new NextResponse(JSON.stringify({ error: 'Unauthorized' }), { status: 401 });
  }

  try {
    // 2️⃣ Query the WhatsApp Business API health endpoint
    const health = await getWhatsAppHealth();

    // 3️⃣ If the webhook is not registered, try to subscribe automatically
    if (!health.webhookRegistered) {
      await ensureWebhook();
    }

    // 4️⃣ Return a compact JSON payload
    return NextResponse.json({
      status: health.status,
      webhookRegistered: health.webhookRegistered,
      lastPing: health.lastPing,
    });
  } catch (err) {
    console.error('WhatsApp status error:', err);
    return new NextResponse(
      JSON.stringify({ error: (err as Error).message }),
      { status: 500 }
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Key points

Decision Reason
App Router (app/api/...) Isolates new routes from legacy catch‑alls, eliminates 404 shadowing.
export const dynamic = 'force-dynamic' Guarantees fresh health data on each request; we never want stale cache.
adminAuth middleware Keeps the endpoint internal – only admins can query WhatsApp health.
ensureWebhook() Auto‑subscribes the webhook if it was removed (e.g., after a token rotation).
NextResponse.json Uses the built‑in response helper for correct Content-Type and serialization.

3. Helper functions (lib/whatsapp.ts)


ts
// lib/

---

*Part of my [Build in Public](https://dev.to/zaerohell) series — sharing the real process of building SaaS projects from Playa del Carmen, México.*

*Repo: `zaerohell/content-automation` · 2026-10-24*

\#playadev #buildinpublic
Enter fullscreen mode Exit fullscreen mode

Top comments (0)