- CORS
- Next.js
How to fix CORS errors in Next.js
Route handlers, rewrites, configuring the API, or a CORS proxy: which fix to use for an Access-Control-Allow-Origin error in a Next.js app, with code for each.
Next.js runs code in two places, and CORS only exists in one of them. Server Components, Route Handlers and Server Actions run on
the server, where CORS does not apply at all. Client Components run in the browser, and a fetch from there to another
origin needs that origin's permission.
So the first question for any CORS error in a Next.js app is: where is this request being made?
'use client';
export function Weather() {
useEffect(() => {
// Runs in the browser, so CORS applies.
fetch('https://api.example.com/forecast?city=berlin')
.then((response) => response.json())
.then(setForecast);
}, []);
// ...
}
This component calls a third-party API from the browser. If that API does not send Access-Control-Allow-Origin for your
origin, the browser blocks the response. Here are the fixes, from most to least preferable.
1. Move the call to the server
Add a Route Handler that calls the upstream API and returns the result. Your component then fetches from its own origin, which is not a cross-origin request at all. As a bonus, any API key lives in a server environment variable rather than in the bundle.
// app/api/forecast/route.ts — runs on the server, where CORS does not apply.
export async function GET(request: Request) {
const city = new URL(request.url).searchParams.get('city') ?? 'berlin';
const upstream = await fetch(
`https://api.example.com/forecast?city=${encodeURIComponent(city)}`,
{
headers: { Authorization: `Bearer ${process.env.FORECAST_API_KEY}` },
next: { revalidate: 300 },
},
);
return Response.json(await upstream.json(), { status: upstream.status });
}
// The component now calls its own origin.
fetch('/api/forecast?city=berlin').then((response) => response.json());
If the data is not user-specific, fetching it directly in a Server Component is simpler still: there is no client request to block.
2. Rewrite the path
A rewrite makes Next.js forward requests for a path on your origin to the upstream API, so the browser only ever talks to your
origin. It needs no handler code, but it cannot add credentials or reshape responses, and it is not available when you deploy
with output: 'export'.
// next.config.ts
import type { NextConfig } from 'next';
const config: NextConfig = {
async rewrites() {
return [{ source: '/forecast-api/:path*', destination: 'https://api.example.com/:path*' }];
},
};
export default config;
3. If you own the API, send CORS headers
When the API is yours, for example a second Next.js app on another domain, the right fix is for it to allow your front end's
origin. Answer the OPTIONS preflight, allow named origins rather than *, and add Vary: Origin.
// app/api/data/route.ts in the API's own Next.js app, called from another origin.
const ALLOWED = new Set(['https://app.example.org']);
function corsHeaders(origin: string | null) {
return origin && ALLOWED.has(origin)
? { 'Access-Control-Allow-Origin': origin, Vary: 'Origin' }
: { Vary: 'Origin' };
}
export async function OPTIONS(request: Request) {
return new Response(null, {
status: 204,
headers: {
...corsHeaders(request.headers.get('origin')),
'Access-Control-Allow-Methods': 'GET, POST',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Max-Age': '600',
},
});
}
export async function GET(request: Request) {
return Response.json({ ok: true }, { headers: corsHeaders(request.headers.get('origin')) });
}
4. Static exports and client-only apps: a CORS proxy
With output: 'export', on a static host, or in a widget embedded on someone else's page, there is no server of yours to
put a route on. A CORS proxy fills that role. With Proxify, prefix the URL and send your public key:
const target = 'https://api.example.com/forecast?city=berlin';
const response = await fetch(
`https://api.proxifyedge.com/proxy?url=${encodeURIComponent(target)}`,
{
headers: {
'X-API-Key': process.env.NEXT_PUBLIC_PROXIFY_KEY!,
// The upstream credential stays in Proxify's vault, not in the bundle.
'X-Proxify-Upstream-Authorization': 'Bearer {{secret.FORECAST_API_KEY}}',
},
},
);
A live Proxify key only answers the origins you list in the dashboard, so it is safe in
NEXT_PUBLIC_ variables. While developing on localhost, switch the key to test, which
answers any origin. Upstream credentials go in the secrets vault and are referenced as
{{secret.NAME}}, so the real value never reaches the browser.
Which fix to use
| Situation | Use |
|---|---|
| The data can be fetched on the server | A Server Component or Route Handler |
| You only need to forward requests, with no secrets | A rewrite |
| You own the API on another origin | CORS headers on the API |
| Static export, client-only app, or embedded widget | A CORS proxy with an origin-locked key |
Summary
CORS errors in Next.js come from requests made in the browser. Move the request to the server when you can; when you cannot, send it through a proxy that adds the headers and keeps your credentials out of the bundle. Try a request against your API in the playground to see exactly which headers come back.