The Right Architecture
Browser to your server to Claude. The API key stays on the server, always.
Every AI feature you build follows the same shape: the browser talks to your server, and your server talks to Claude. The browser never talks to Claude directly.
Why the middle layer
- Security — the API key lives on the server, never in the browser.
- Control — you add auth, rate limits, and validation.
- Flexibility — you can log, cache, and shape prompts before they reach Claude.
Example
// server.js — your backend endpoint the browser calls
import express from "express";
import Anthropic from "@anthropic-ai/sdk";
const app = express();
app.use(express.json());
const client = new Anthropic(); // key stays here, on the server
app.post("/api/ask", async (req, res) => {
const result = await client.messages.create({
model: "claude-opus-4-8", max_tokens: 1024,
messages: [{ role: "user", content: req.body.question }],
});
const text = result.content.find((b) => b.type === "text")?.text ?? "";
res.json({ answer: text });
});
app.listen(3000);When to use it
- Build a Next.js app with an /api/ask route that proxies user messages to Claude, keeping the key server-side.
- Create an Express middleware layer that rate-limits requests before forwarding them to the Anthropic API.
- Deploy your Claude backend as a separate microservice so multiple frontend apps share one secure gateway.
More examples
Next.js API route as Claude proxy
The API route sits between the browser and Anthropic — the key never leaves the server.
// src/app/api/ask/route.ts
import { NextRequest, NextResponse } from "next/server";
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic(); // key from env — never sent to browser
export async function POST(req: NextRequest) {
const { question } = await req.json();
const msg = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 512,
messages: [{ role: "user", content: question }],
});
return NextResponse.json({ answer: msg.content[0].text });
}Browser calls your server, not Claude
The browser only knows about your /api/ask endpoint — the Anthropic URL and key are never visible in the client.
// Browser-side fetch — calls YOUR server, not Anthropic
async function ask(question) {
const res = await fetch("/api/ask", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ question }),
});
const { answer } = await res.json();
return answer;
}Add auth before proxying to Claude
Checks the session before calling Claude so unauthenticated users cannot consume API credits.
export async function POST(req: NextRequest) {
const session = await getServerSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const { question } = await req.json();
const msg = await client.messages.create({
model: "claude-haiku-4-5",
max_tokens: 256,
messages: [{ role: "user", content: question }],
});
return NextResponse.json({ answer: msg.content[0].text });
}
Discussion