From 52e4d4c0f5409b4f8a46a23a24f6db97119f2560 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Tue, 6 Oct 2026 18:41:25 +0000 Subject: [PATCH] Add GET /api/health for the status page Only /api/health/db existed, so /api/health 404'd. The new route answers {status:ok, db:ok} with Cache-Control: no-store after SELECT 1, or 503 {status:error, db:down} if the database fails or takes over 3s, with no error details. Co-Authored-By: Claude Opus 5.5 --- apps/web/app/api/health/route.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 apps/web/app/api/health/route.ts diff --git a/apps/web/app/api/health/route.ts b/apps/web/app/api/health/route.ts new file mode 100644 index 0000000..2a05c63 --- /dev/null +++ b/apps/web/app/api/health/route.ts @@ -0,0 +1,29 @@ +import { NextResponse } from 'next/server'; +import { getDb } from '@/lib/db'; + +// Never cache: a cached 200 would keep reporting health after the app goes away. +export const dynamic = 'force-dynamic'; + +const NO_STORE = { 'Cache-Control': 'no-store' }; + +/** + * Liveness for status.profullstack.com: the app answers and the database + * answers `SELECT 1` within 3s. Unlike /api/health/db it never says why it + * failed. + */ +export async function GET() { + let timer: ReturnType | undefined; + try { + await Promise.race([ + getDb().sql`SELECT 1`, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error('timeout')), 3000); + }), + ]); + return NextResponse.json({ status: 'ok', db: 'ok' }, { headers: NO_STORE }); + } catch { + return NextResponse.json({ status: 'error', db: 'down' }, { status: 503, headers: NO_STORE }); + } finally { + clearTimeout(timer); + } +}