What is the correct way to set up CORS for a Next.js frontend on Vercel and an Express backend on Render in a production environment?

I’m encountering persistent CORS problems with my application, which functions correctly in a local environment. When deployed, requests from my Next.js frontend hosted on Vercel to my Express backend on Render face CORS restrictions. Despite applying all standard fixes, such as configuring Access-Control-Allow-Origin to allow all origins or redeploying, issues persist. My tech stack includes: Frontend: Next.js on Vercel, Backend: Node.js/Express on Render with JWT for authentication. In production, I’m receiving an error stating: ‘Access to XMLHttpRequest at ‘https://ai-pulse-backend.onrender.com/api/v1/auth/login’ from origin ‘https://ai-pulse-frontend.vercel.app’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.’ Additionally, I’ve observed: CORS request did not succeed. Status code: (null).

In my Express backend, I’ve implemented CORS middleware using the cors package, allowing the frontend domain and enabling credentials. I’ve attempted to send required headers like Authorization and Content-Type from both sides. I verified the use of withCredentials: true for cookies and confirmed that endpoints function properly using Postman. Despite my setup, issues still arise. Here’s the CORS implementation in my Express backend:

import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import routes from './routes/index';

const app = express();
const port = process.env.PORT || 8080;

const corsConfig = {
  origin: ['http://localhost:3000', 'https://ai-pulse-frontend.vercel.app'], // Update this to your frontend URL
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization'],
};

app.use(cors(corsConfig));
app.use(express.json());
app.use(cookieParser());

app.use((req, res, next) => {
  const origin = req.headers.origin;
  if (origin && corsConfig.origin.includes(origin)) {
    res.header('Access-Control-Allow-Origin', origin);
  }
  res.header('Access-Control-Allow-Credentials', 'true');
  res.header('Access-Control-Allow-Methods', 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS');
  res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');

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

app.use(routes);

My Axios configuration on the frontend is set up to include credentials:

import axios from 'axios';

const API_URL = 'https://ai-pulse-backend.onrender.com/api/v1';

const axiosInstance = axios.create({
  baseURL: API_URL,
  withCredentials: true,
});

export default axiosInstance;

I also use Zustand for authentication:

login: async (email, password) => {
  const response = await axiosInstance.post('/auth/login', { email, password });
  const { accessToken } = response.data;
  axiosInstance.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
}

Are there any extra headers or settings required for secure JWT authentication handling over CORS? Any advice or solutions would be immensely helpful! Thank you!

From my experience, a possible issue could be related to the preflight OPTIONS request. While your server seems configured to handle CORS, sometimes network layers and reverse proxies might interfere. Ensure that any proxy or network component, including Vercel configurations, allows CORS headers. Moreover, ensure the backend URL does not redirect to a base URL without proper CORS handling. You might also want to inspect network calls in the browser developer console to identify any overlooked discrepancies in request headers. Finally, deploying using the exact production environment settings locally for troubleshooting can sometimes reveal hidden misconfigurations.