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 directly | How the proxy solves it |
|---|---|
CORS — FastAPI has no CORSMiddleware, so cross-origin browser calls fail | browser calls /api/agents on its own origin |
| Backend topology leaks into client bundles | AGENTOPS_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 browser | the 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
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/500pass 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 →
502with adetailshaped 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:
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).
Related pages
- Components: CreateAgentForm — the caller
- Backend Schemas — the payload contract being forwarded
- Configuration —
AGENTOPS_API_BASE_URL