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.
To fix CORS errors in Next.js, first work out where the request runs. 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. If the mechanics are new to you, start with what CORS is. 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')) });
}
If your app sends cookies to that API, read CORS with credentials before you ship: the wildcard stops working and a few extra headers become mandatory.
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 ProxifyEdge, 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 ProxifyEdge's vault, not in the bundle.
'X-Proxify-Upstream-Authorization': 'Bearer {{secret.FORECAST_API_KEY}}',
},
});
A live ProxifyEdge 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 |
Key takeaways
- CORS errors in Next.js come from requests made in the browser, never from server code.
- Move the request to a Server Component or Route Handler whenever you can.
- If you own the API, allow named origins and answer
OPTIONSyourself. - When there is no server of yours, use a proxy that adds the headers and keeps credentials out of the bundle.
Try a request against your API in the playground to see exactly which headers come back.