How to Fix CORS Preflight Errors When Calling AI APIs from the Browser
Trying to call the OpenAI or Anthropic API directly from client-side JavaScript almost always fails with a CORS preflight error, and unlike many CORS issues that come from a misconfiguration, this one is deliberate on the provider's side — these APIs generally don't allow direct browser-to-API calls at all, for a reason worth understanding before trying to work around it.
The core issue is that calling these APIs requires a secret API key attached to every request. If that key were embedded in client-side JavaScript to make a direct browser call work, it would be visible to anyone who opens their browser's developer tools — effectively publishing your API key (and your billing) to every visitor. Providers block direct browser calls specifically to prevent this exact mistake, not because of an oversight in their CORS configuration.
This means the CORS error you're seeing is a symptom of an architectural issue, not something fixable by adjusting headers or adding a CORS proxy. The correct fix is routing the request through your own backend: the browser calls your server, your server (which holds the real API key safely) calls OpenAI or Anthropic, and the response comes back through your server to the browser. The API key never reaches client-side code at any point.
In a Next.js application, this is a natural fit for a Route Handler — a simple API route that receives the browser's request, forwards it to the AI provider with the server-side API key attached, and streams or returns the response. This adds one hop but solves the CORS problem entirely, since server-to-server requests aren't subject to browser CORS restrictions at all.
For streaming responses specifically — which most chat-style AI interfaces want, so text appears incrementally rather than all at once — the Route Handler needs to properly forward the streamed response rather than buffering the entire response before sending it to the client. Most AI provider SDKs support streaming natively; the Route Handler's job is to pass that stream through to the browser using the appropriate streaming response APIs rather than accidentally awaiting the whole thing first.
It's worth adding your own rate limiting and basic abuse protection at this backend layer too, since it's now the single entry point for all AI API calls from your app — without it, a single user (or a bot) hammering your endpoint could exhaust your API quota or run up unexpected costs, a risk that didn't exist in early prototyping but becomes real the moment the feature is public.
If you're seeing a CORS error specifically while testing locally and wondering whether it's just a local development quirk — it isn't. The same restriction applies in production; local development just happens to be where most people first attempt a direct browser call and discover the limitation, before building the backend proxy that any real deployment needs anyway.
Found this helpful?
SyncTonight's tools and guides are free and always will be. If this post saved you some debugging time, a coffee goes a long way — no pressure, just appreciated.
☕ Buy me a coffee