src/lib/api-route.ts
Linked repository file from this build.
import { NextResponse } from "next/server";
import { z } from "zod";
import { AccessError } from "@/lib/auth/scope";
import { DocumentError } from "@/lib/documents/server-storage";
import { NotFoundError, RuleError } from "@/lib/errors";
import { StaleReviewError } from "@/features/reviews/persistence";
import { ReviewStoreUnavailableError } from "@/features/reviews/review-store";
/**
* Wrap an API handler so every route maps errors the same way:
* typed errors keep their status and message, and anything else is logged
* and returns 503 with `fallback`.
*/
export function apiRoute<Context = unknown>(
fallback: string,
handler: (request: Request, context: Context) => Promise<Response>,
) {
return async (request: Request, context: Context) => {
try {
return await handler(request, context);
} catch (error) {
return errorResponse(error, fallback);
}
};
}
export function errorResponse(error: unknown, fallback: string): Response {
if (error instanceof AccessError || error instanceof DocumentError)
return NextResponse.json({ error: error.message }, { status: error.status });
if (error instanceof NotFoundError)
return NextResponse.json({ error: error.message }, { status: 404 });
if (error instanceof StaleReviewError)
return NextResponse.json({ error: error.message, current: error.current }, { status: 409 });
if (error instanceof ReviewStoreUnavailableError)
return NextResponse.json({ error: error.message }, { status: 503 });
// The client turns serialized Zod issues into readable field messages.
if (error instanceof RuleError || error instanceof z.ZodError)
return NextResponse.json({ error: error.message }, { status: 400 });
if (error instanceof SyntaxError)
return NextResponse.json({ error: "The request body is not valid JSON." }, { status: 400 });
return unavailableResponse(error, fallback);
}
/** Log the real failure and, off production, return it so Preview Network can show the cause. */
export function unavailableResponse(error: unknown, fallback: string) {
console.error(fallback, {
name: error instanceof Error ? error.name : typeof error,
message: error instanceof Error ? error.message : String(error),
databaseUrlConfigured: Boolean(process.env.DATABASE_URL),
vercelEnv: process.env.VERCEL_ENV ?? "local",
});
if (error instanceof Error && error.stack) console.error(error.stack);
return NextResponse.json(
process.env.VERCEL_ENV === "production"
? { error: fallback }
: {
error: fallback,
detail: error instanceof Error ? error.message : String(error),
},
{ status: 503 },
);
}