From 30d08c665d824ffd684d0e5a7546cb10d32d7775 Mon Sep 17 00:00:00 2001 From: gosangam Date: Mon, 11 May 2026 17:28:04 +0530 Subject: [PATCH] feat: add proxy to url shortner --- app/api/share/route.ts | 57 ++++++++++++++++++++++++++++++++++++++++++ lib/bff.ts | 37 --------------------------- lib/share.ts | 30 ++++++++++++++++------ 3 files changed, 80 insertions(+), 44 deletions(-) create mode 100644 app/api/share/route.ts diff --git a/app/api/share/route.ts b/app/api/share/route.ts new file mode 100644 index 00000000..6549ebbf --- /dev/null +++ b/app/api/share/route.ts @@ -0,0 +1,57 @@ +import { NextRequest, NextResponse } from "next/server"; + +// Same-origin proxy for the URL-shortener create endpoint. Client components +// (text-selection toolbar, share modal) call /api/share so the actual BFF +// request happens server-side — this is the only way to read `BFF_URL`, +// since Next.js does not expose non-`NEXT_PUBLIC_` env vars to the browser +// bundle. Same pattern as /api/search. +const BFF_BASE = process.env.BFF_URL || "http://localhost:8080"; + +export async function POST(request: NextRequest) { + let body: unknown; + try { + body = await request.json(); + } catch { + return NextResponse.json( + { success: false, error: { code: 400, message: "invalid JSON body" } }, + { status: 400 }, + ); + } + + const url = + body && typeof body === "object" && "url" in body + ? (body as { url: unknown }).url + : null; + if (typeof url !== "string" || !url.startsWith("/")) { + return NextResponse.json( + { + success: false, + error: { code: 400, message: "url must be a same-site path" }, + }, + { status: 400 }, + ); + } + + try { + const res = await fetch(new URL("/api/v1/share", BFF_BASE).toString(), { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ url }), + cache: "no-store", + }); + + const text = await res.text(); + return new NextResponse(text, { + status: res.status, + headers: { "Content-Type": "application/json" }, + }); + } catch { + return NextResponse.json( + { success: false, error: { code: 502, message: "BFF unreachable" } }, + { status: 502 }, + ); + } +} diff --git a/lib/bff.ts b/lib/bff.ts index 22614d08..5f56c663 100644 --- a/lib/bff.ts +++ b/lib/bff.ts @@ -122,40 +122,3 @@ export async function bffList( return result ?? []; } -/** - * POST JSON to the BFF API. Used for mutations like the URL shortener; does - * not participate in Next.js caching (each POST is a unique side-effect). - * - * Same envelope-unwrapping behavior as bff(): returns the inner `data` - * value, throws BffError on non-2xx. - */ -export async function bffPost( - path: string, - body: Record, -): Promise { - const url = apiUrl(path); - - const res = await fetch(url, { - method: "POST", - headers: { - Accept: "application/json", - "Content-Type": "application/json", - }, - body: JSON.stringify(body), - cache: "no-store", - }); - - if (!res.ok) { - const text = await res.text().catch(() => ""); - throw new BffError( - res.status, - `BFF ${res.status} POST ${path}: ${text.slice(0, 200)}`, - ); - } - - const json = await res.json(); - if (json && typeof json === "object" && "data" in json) { - return json.data as T; - } - return json as T; -} diff --git a/lib/share.ts b/lib/share.ts index c9a359b1..b97b23fe 100644 --- a/lib/share.ts +++ b/lib/share.ts @@ -1,18 +1,22 @@ /** - * Short link client — talks to the BFF /share endpoints. + * Short link client. * - * Used by the text-selection toolbar (`buildShortHighlightUrl`) and the - * `/share/[code]` page (`resolveShortLink`). + * - `createShortLink` runs in the browser and POSTs to `/api/share`, a Next.js + * route handler that proxies to the BFF. We can't call the BFF directly + * from the browser because `BFF_URL` is not exposed to the client bundle + * (no `NEXT_PUBLIC_` prefix). Same-origin proxy also avoids CORS. + * + * - `resolveShortLink` is only called from the `/share/[code]` server + * component, so it can use `bff()` directly with the server-side env var. */ -import { bff, bffPost } from "@/lib/bff"; +import { bff } from "@/lib/bff"; // Module-scoped cache: a single user shouldn't generate a fresh code every // time they re-click Share on the same selection. Keyed by the relative path // (including ?hl=…), this returns the same promise for repeat calls. const createCache = new Map>(); -type CreateResponse = { code: string }; type ResolveResponse = { url: string }; /** @@ -23,8 +27,20 @@ export function createShortLink(longPath: string): Promise { const cached = createCache.get(longPath); if (cached) return cached; - const promise = bffPost("/share", { url: longPath }) - .then((r) => r.code) + const promise = fetch("/api/share", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ url: longPath }), + }) + .then(async (res) => { + if (!res.ok) return null; + const json = await res.json().catch(() => null); + const code = json?.data?.code; + return typeof code === "string" ? code : null; + }) .catch(() => null); createCache.set(longPath, promise);