/** * BFF API client for server components. * * Provides typed fetch helpers that call the Go BFF API server. * Used by all data layer files (lib/*.ts) instead of direct Prisma queries. * * The BFF URL is configured via the BFF_URL environment variable. */ const BFF_BASE = process.env.BFF_URL || "http://localhost:8080"; /** Build a full BFF API URL. */ function apiUrl(path: string, params?: Record): string { const url = new URL(`/api/v1${path}`, BFF_BASE); if (params) { for (const [key, value] of Object.entries(params)) { url.searchParams.set(key, value); } } return url.toString(); } /** Custom error for BFF API failures. */ export class BffError extends Error { constructor( public status: number, message: string, ) { super(message); this.name = "BffError"; } } /** * Fetch JSON from the BFF API. * * - Throws BffError on non-2xx responses * - Returns null for 404 responses (instead of throwing) * - Uses Next.js fetch with revalidation options * * @param path - API path (e.g., "/books/bg") * @param options - Optional fetch options * @param params - Optional query parameters */ export async function bff( path: string, options?: { params?: Record; /** Next.js revalidation in seconds. Default: 60. Set to 0 for no cache. */ revalidate?: number; /** Next.js cache tags for on-demand revalidation */ tags?: string[]; }, ): Promise { const url = apiUrl(path, options?.params); const res = await fetch(url, { next: { revalidate: options?.revalidate ?? 60, tags: options?.tags, }, headers: { Accept: "application/json", }, }); if (res.status === 404) { return null; } if (!res.ok) { const body = await res.text().catch(() => ""); throw new BffError( res.status, `BFF ${res.status} ${path}: ${body.slice(0, 200)}`, ); } const json = await res.json(); // Unwrap BFF envelope: { success: true, data: T } if (json && typeof json === "object" && "data" in json) { return json.data as T; } // Fallback: return raw response if no envelope return json as T; } /** * Fetch JSON from the BFF API, throwing on 404. * Use this when a missing resource should be an error, not null. */ export async function bffOrThrow( path: string, options?: { params?: Record; revalidate?: number; tags?: string[]; }, ): Promise { const result = await bff(path, options); if (result === null) { throw new BffError(404, `Not found: ${path}`); } return result; } /** * Fetch a list from the BFF API. * Returns empty array on 404 (instead of null). */ export async function bffList( path: string, options?: { params?: Record; revalidate?: number; tags?: string[]; }, ): Promise { const result = await bff(path, options); return result ?? []; }