Skip to content
3 min read · 565 words

API Proxy

Browser writes never touch FastAPI directly. They hit a same-origin Next.js route handler that forwards the request server-side, relays the exact status and body, and converts "backend unreachable" into a clean 502.

Why a proxy at all?

Problem with browser → FastAPI directlyHow the proxy solves it
CORS — FastAPI has no CORSMiddleware, so cross-origin browser calls failbrowser calls /api/agents on its own origin
Backend topology leaks into client bundlesAGENTOPS_API_BASE_URL is read server-side only
FastAPI error shapes are inconsistent for UIs (string vs. validation array)one place to relay/normalize
Future auth would need tokens in the browserthe BFF is where session→service auth will live

Reads (dashboard, agents list) skip the proxy because they already execute on the server inside server components — same trust zone as the route handler. See Data Fetching.

The implementation

ts
const API_BASE_URL = process.env.AGENTOPS_API_BASE_URL ?? "http://localhost:8001";
 
export async function POST(request: Request) {
  let payload: CreateAgentRequest;
  try {
    payload = (await request.json()) as CreateAgentRequest;
  } catch {
    return NextResponse.json({ detail: "Invalid JSON request body" }, { status: 400 });
  }
 
  try {
    const response = await fetch(`${API_BASE_URL}/agents/`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
      cache: "no-store",
    });
 
    const responseText = await response.text();
    let data: unknown = null;
    try {
      data = responseText ? JSON.parse(responseText) : null;
    } catch {
      data = { detail: responseText || "Unexpected backend response" };
    }
    return NextResponse.json(data, { status: response.status });   // exact relay
  } catch {
    return NextResponse.json(
      { detail: `Unable to reach ${API_BASE_URL}/agents/` },
      { status: 502 },
    );
  }
}

Design points:

  • Status transparency — backend 200/422/500 pass through unchanged, so the form can trust FastAPI semantics.
  • Body resilience — the response is read as text first, then JSON-parsed; a non-JSON body (proxy pages, crashes) becomes {detail: <text>} instead of a parse exception.
  • Network failure → 502 with a detail shaped like every other error — the UI needs only one error format.
  • cache: "no-store" — a mutation must never be cached.

Error normalization (client side)

The form's getApiErrorMessage completes the pipeline, accepting both FastAPI shapes:

{"detail": "This agent does not have ..."}            → shown as-is
{"detail": [{"msg": "Field required", ...}, ...]}     → msgs joined into one line
anything else                                          → "Unable to create agent."

End-to-end flow:

Rendering diagram…

Extending the pattern

Every new browser-initiated backend call gets a sibling handler — app/api/agents/[id]/chat/route.ts, app/api/agents/[id]/documents/route.ts, etc. — following the same skeleton: parse defensively, forward with no-store, relay status/body, 502 on unreachable. Full walkthrough with code: Add an Endpoint.

Security notes

The proxy currently adds no auth and forwards any JSON body — it inherits the platform's open security posture. It is, however, exactly where per-user auth headers will be attached when authentication lands (the browser gets a session; the proxy holds service credentials).