The proxy pattern: the browser never holds the key.
The client calls your endpoint; your endpoint holds the key in an environment
variable, applies your own auth and rate limit, and forwards the request. A minimal
API route version:
// /api/generate - runs on the server. The key lives in env, never in the bundle.
export async function POST(req) {
const user = await requireUser(req); // your auth check
await enforceRateLimit(user.id); // your rate limit
const { prompt } = await req.json();
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY, // server-side env var
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-5",
max_tokens: 1024,
messages: [{ role: "user", content: prompt }],
}),
});
return new Response(res.body, { status: res.status });
}
The two lines that make this production-grade are the auth check and the rate limit.
A proxy without them is an open relay: attackers do not need your key if your endpoint
will spend your money for anyone who asks. This exact pattern works as a Next.js
route, a Supabase Edge Function, or a Cloudflare Worker.