Calling AI From the Server
Keep AI API keys off the client by calling the provider from a server route your React app fetches.
TL;DR
- Never expose an AI
apiKeyin browser JavaScript. - Call the provider from a server
routeHandler, not the client. - The client fetches your
/api/chat, which calls the model.
The Client Fetches Your Route
Post the PromptThe React client only ever talks to your own route.
await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ prompt }),
});No Secrets in ClientThe browser never sees or sends the API key.
// zero apiKey anywhere in the bundleRead the AnswerAwait JSON, or read a stream for live tokens.
const { text } = await res.json();Keep the Key Server-Side
Server-Only EnvStore the key where the client cannot read it.
# .env.local
AI_API_KEY=sk-...No Public PrefixNever use NEXT_PUBLIC_ for a secret value.
// NEXT_PUBLIC_* ships to the browserRead at RuntimeThe handler reads the key on the server.
const key = process.env.AI_API_KEY;Build the Route Handler
Define the RouteA POST handler receives the user's prompt.
export async function POST(req: Request) {
const { prompt } = await req.json();
}Call the ProviderUse the server SDK with your secret key.
const out = await callModel(prompt, key);Return the ResultSend JSON or a streaming Response back.
return Response.json({ text: out });Add Guards
Validate InputReject empty or oversized prompts early.
if (!prompt || prompt.length > 4000)
return new Response('Bad', { status: 400 });Require AuthCheck a session before spending tokens.
if (!session)
return new Response('Unauth', { status: 401 });Rate LimitCap requests per user to control cost.
if (await overLimit(userId))
return new Response('Slow', { status: 429 });Tips
- Read the key from a server
process.envvariable that is never prefixed for the client, so it never ships in the bundle. - Run the route on the
edgeruntime for low latency and native streaming when your provider and SDK support it.
Warnings
- A key in a
NEXT_PUBLIC_variable or client fetch is public; anyone can read it from the network tab and rack up charges. - Do not skip input validation and auth on the route; an open
/api/chatis a free proxy for anyone to abuse.
In Practice
The client posts a prompt to a server route; the route calls the model with a server-only key.
- The client fetch only targets /api/chat, never the provider directly.
- The route reads the key from server env, hidden from the browser.
- The provider call runs on the server, then JSON returns to React.
- Add auth and rate limits on the route before shipping to production.
// app/api/chat/route.ts (server)
export async function POST(req: Request) {
const { prompt } = await req.json();
const key = process.env.AI_API_KEY!;
const text = await callModel(prompt, key);
return Response.json({ text });
}
// Component (client)
async function ask(prompt: string) {
const res = await fetch('/api/chat', {
method: 'POST',
body: JSON.stringify({ prompt }),
});
return (await res.json()).text;
}FAQ
Because your API key would ship to the browser, where anyone can steal it. Always call the provider from a server route, such as a Next.js route handler. The React client fetches your route, and the key stays in a server-only env variable.
In a server-only environment variable, never one exposed to the client. In Next.js use process.env.AI_API_KEY, not a NEXT_PUBLIC_ name. The handler reads it on the server at request time, so it never appears in the client bundle.
Barely, and it unlocks big wins: you can cache responses, add rate limiting, and stream from the edge close to users. The tiny extra hop is far outweighed by the security and performance it enables.