Skip to content

Fix CORS errors between localhost ports in development

fix

Fetch requests between different localhost ports blocked by CORS policy with no Access-Control-Allow-Origin header

nextjscorsapidevelopment
32 views

Problem

Fetch requests from the frontend dev server to a backend API on a different port fail with a CORS error:

Access to fetch at 'http://localhost:4000/api/users' from origin 'http://localhost:3000'
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on
the requested resource.

The fetch call that triggers the error:

// app/dashboard/page.tsx
const res = await fetch("http://localhost:4000/api/users", {
  headers: { "Content-Type": "application/json" },
});
// TypeError: Failed to fetch (CORS blocked)

Solution

Option 1: Proxy through Next.js rewrites (recommended)

// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  rewrites: async () => [
    {
      source: "/api/:path*",
      destination: "http://localhost:4000/api/:path*",
    },
  ],
};

export default nextConfig;

Then update the fetch call to use a relative URL:

// app/dashboard/page.tsx
const res = await fetch("/api/users", {
  headers: { "Content-Type": "application/json" },
});

Option 2: Add CORS headers on the API server

// server.ts (Express)
import cors from "cors";
import express from "express";

const app = express();

app.use(
  cors({
    origin: "http://localhost:3000",
    methods: ["GET", "POST", "PUT", "DELETE"],
  })
);

Why It Works

The browser's Same-Origin Policy treats different ports as different origins, so http://localhost:3000 and http://localhost:4000 are cross-origin. Next.js rewrites proxy the request through the frontend's own server, so the browser sees a same-origin request to /api/users and never triggers CORS checks. The actual cross-origin request happens server-to-server where CORS does not apply. With Option 2, the backend explicitly sends Access-Control-Allow-Origin headers permitting the frontend origin, so the browser allows the response through.

Context

  • Applies to any local development setup with separate frontend and backend servers on different ports
  • Vite offers a similar server.proxy option in vite.config.ts for the same purpose
  • Preflight OPTIONS requests are sent by the browser for non-simple requests (custom headers, PUT/DELETE methods) -- the API server must respond to these as well for Option 2
  • In production, CORS is typically not an issue if the frontend and API share the same domain or are behind a reverse proxy
  • Do not use origin: "*" with credentials: true -- browsers reject this combination for security reasons
About this share
Contributormblode
Repositorymblode/shares
CreatedFeb 9, 2026
Environmentnextjs
View on GitHub