Skip to content

Fix: Next.js CORS Error on API Routes

FixDevs ·

Quick Answer

How to fix CORS errors in Next.js API routes — adding Access-Control headers, handling preflight OPTIONS requests, configuring next.config.js headers, and avoiding common proxy mistakes.

The Error

A browser fetch or axios request to a Next.js API route fails with a CORS error:

Access to fetch at 'https://api.example.com/api/users' from origin 'https://app.example.com'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.

Or the preflight OPTIONS request fails:

Access to XMLHttpRequest at 'https://api.example.com/api/data' from origin
'https://app.example.com' has been blocked by CORS policy: Response to preflight
request doesn't pass access control check: It does not have HTTP ok status.

In the Network tab, the failed request shows:

  • Method: OPTIONS (preflight), Status: 405 Method Not Allowed
  • Or Method: GET/POST, Status: 200 but the response is blocked because of missing CORS headers

Why This Happens

CORS (Cross-Origin Resource Sharing) is a browser security mechanism. When your frontend (on app.example.com) makes a request to an API (on api.example.com), the browser first checks whether the server allows cross-origin requests by looking for Access-Control-Allow-Origin in the response headers.

Next.js API routes don’t set CORS headers by default. Common causes:

  • Missing CORS headers — the API route returns a response without Access-Control-Allow-Origin.
  • Preflight not handled — for requests with custom headers or non-simple methods (PUT, DELETE, PATCH), browsers send an OPTIONS preflight request first. If your route doesn’t handle OPTIONS, Next.js returns 405.
  • Wildcard origin with credentialsAccess-Control-Allow-Origin: * doesn’t work when credentials: 'include' is used. You must specify the exact origin.
  • Frontend and API on different domains — during development, the frontend runs on localhost:3000 and calls an API on localhost:4000 — different ports = different origins.
  • Missing headers in next.config.js — if you configure CORS in next.config.js headers but the pattern doesn’t match your API routes, the headers won’t be added.

Fix 1: Add CORS Headers Directly in the API Route

The most straightforward fix: set the headers in each route handler. For the App Router:

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';

const corsHeaders = {
  'Access-Control-Allow-Origin': 'https://app.example.com',
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
  'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};

// Handle preflight
export async function OPTIONS() {
  return NextResponse.json({}, { headers: corsHeaders });
}

export async function GET(request: NextRequest) {
  const users = await getUsers();
  return NextResponse.json(users, { headers: corsHeaders });
}

export async function POST(request: NextRequest) {
  const body = await request.json();
  const user = await createUser(body);
  return NextResponse.json(user, { status: 201, headers: corsHeaders });
}

For development, allow any origin:

const allowedOrigins = ['https://app.example.com', 'http://localhost:3000'];

function getCorsHeaders(request: NextRequest) {
  const origin = request.headers.get('origin') ?? '';
  const isAllowed = allowedOrigins.includes(origin);
  return {
    'Access-Control-Allow-Origin': isAllowed ? origin : allowedOrigins[0],
    'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
    'Access-Control-Allow-Headers': 'Content-Type, Authorization',
  };
}

For the Pages Router (pages/api/):

// pages/api/users.ts
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  // Set CORS headers
  res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');

  // Handle preflight
  if (req.method === 'OPTIONS') {
    res.status(200).end();
    return;
  }

  if (req.method === 'GET') {
    res.json({ users: [] });
  } else {
    res.status(405).json({ error: 'Method not allowed' });
  }
}

Fix 2: Create a CORS Middleware Helper

To avoid repeating CORS headers in every route, create a reusable helper:

For the App Router:

// lib/cors.ts
import { NextRequest, NextResponse } from 'next/server';

type CorsOptions = {
  origin?: string | string[];
  methods?: string[];
  headers?: string[];
};

export function withCors(
  handler: (req: NextRequest) => Promise<NextResponse>,
  options: CorsOptions = {}
) {
  const {
    origin = '*',
    methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
    headers = ['Content-Type', 'Authorization'],
  } = options;

  return async (request: NextRequest): Promise<NextResponse> => {
    const requestOrigin = request.headers.get('origin');

    const allowOrigin = Array.isArray(origin)
      ? (origin.includes(requestOrigin ?? '') ? requestOrigin : origin[0]) ?? '*'
      : origin;

    const corsHeaders = {
      'Access-Control-Allow-Origin': allowOrigin,
      'Access-Control-Allow-Methods': methods.join(', '),
      'Access-Control-Allow-Headers': headers.join(', '),
    };

    if (request.method === 'OPTIONS') {
      return NextResponse.json({}, { headers: corsHeaders });
    }

    const response = await handler(request);

    Object.entries(corsHeaders).forEach(([key, value]) => {
      response.headers.set(key, value);
    });

    return response;
  };
}

Use it in your routes:

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withCors } from '@/lib/cors';

async function getUsers(request: NextRequest) {
  const users = await db.user.findMany();
  return NextResponse.json(users);
}

export const GET = withCors(getUsers, {
  origin: ['https://app.example.com', 'http://localhost:3000'],
});

Fix 3: Configure CORS in next.config.js

For a centralized approach that applies to all API routes without modifying each one, use headers() in your Next.js config:

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  async headers() {
    return [
      {
        // Apply to all API routes
        source: '/api/:path*',
        headers: [
          { key: 'Access-Control-Allow-Origin', value: 'https://app.example.com' },
          { key: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' },
          { key: 'Access-Control-Allow-Headers', value: 'Content-Type, Authorization' },
        ],
      },
    ];
  },
};

module.exports = nextConfig;

Limitation: The headers() config doesn’t handle preflight OPTIONS requests — Next.js returns 405 for OPTIONS by default. You still need to add OPTIONS handlers in each route, or use middleware.

Fix 4: Use Next.js Middleware for Global CORS

Middleware runs before every request, making it the cleanest place for global CORS handling:

// middleware.ts (in project root, not src/)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const allowedOrigins = [
  'https://app.example.com',
  'http://localhost:3000',
];

export function middleware(request: NextRequest) {
  const origin = request.headers.get('origin') ?? '';
  const isAllowed = allowedOrigins.includes(origin);

  // Handle preflight OPTIONS
  if (request.method === 'OPTIONS') {
    const response = new NextResponse(null, { status: 200 });
    if (isAllowed) {
      response.headers.set('Access-Control-Allow-Origin', origin);
    }
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
    response.headers.set('Access-Control-Max-Age', '86400');
    return response;
  }

  const response = NextResponse.next();

  if (isAllowed) {
    response.headers.set('Access-Control-Allow-Origin', origin);
    response.headers.set('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
    response.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  }

  return response;
}

export const config = {
  matcher: '/api/:path*',   // Only apply to API routes
};

This handles both regular requests and OPTIONS preflight in one place.

Fix 5: Fix CORS with Credentials

If you send cookies or Authorization headers with credentials: 'include', the wildcard origin * won’t work — the browser requires a specific origin:

// Client-side fetch
fetch('https://api.example.com/api/profile', {
  credentials: 'include',   // Sends cookies
  headers: { Authorization: `Bearer ${token}` },
});

Server must respond with the specific origin and credentials flag:

// app/api/profile/route.ts
export async function GET(request: NextRequest) {
  const origin = request.headers.get('origin') ?? '';

  return NextResponse.json(data, {
    headers: {
      'Access-Control-Allow-Origin': origin,           // Specific origin, not '*'
      'Access-Control-Allow-Credentials': 'true',      // Required for credentials
      'Access-Control-Allow-Methods': 'GET, OPTIONS',
      'Access-Control-Allow-Headers': 'Content-Type, Authorization',
    },
  });
}

Warning: Setting Access-Control-Allow-Origin to the request’s origin header without validation allows any origin. Always validate against an allowlist before reflecting the origin back.

Fix 6: Use a Proxy for Same-Origin Requests

The cleanest solution for a Next.js frontend calling its own backend is to route through Next.js itself — no CORS at all because both frontend and API are on the same origin:

// next.config.js — proxy /api/external to another server
const nextConfig = {
  async rewrites() {
    return [
      {
        source: '/api/external/:path*',
        destination: 'https://api.external-service.com/:path*',
      },
    ];
  },
};

Your frontend calls /api/external/users (same origin), Next.js proxies it to the external API server-side — no browser CORS check.

Still Not Working?

Confirm the CORS headers are actually in the response — open DevTools → Network → click the failed request → check Response Headers. If Access-Control-Allow-Origin is missing, the headers aren’t being set by your code.

Check for multiple CORS headers. Some proxy configurations (nginx, Cloudflare) add CORS headers, and if your Next.js code also adds them, you get duplicate headers like:

Access-Control-Allow-Origin: https://app.example.com, *

Multiple values for Access-Control-Allow-Origin are invalid. Remove CORS headers from one layer.

Check the OPTIONS preflight response specifically. In DevTools Network tab, filter by “OPTIONS” method. If it returns 405, your route isn’t handling OPTIONS. If it returns 200 but the main request still fails, check that the Access-Control-Allow-Headers includes every custom header your request sends.

Verify the origin header is present. Browsers only send the Origin header for cross-origin requests. If you’re testing with curl or Postman, add -H "Origin: http://localhost:3000" to trigger CORS header logic.

For related Next.js issues, see Fix: Next.js API Route Not Working and Fix: Next.js Environment Variables Not Working.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles