If you have ever created a backend and a frontend that both live on different servers trying to communicate with each other, then you will know of CORS issues.
Cross-Origin Resource Sharing (CORS) is an HTTP-header based mechanism that allows a server to indicate any origins (domain, scheme, or port) other than its own from which a browser should permit loading resources.
If you have started working with NextJS 13, you’ll also notice that there are a bunch of changes which makes it a bit harder to get up and running with existing libraries, such as nextjs-cors.
I’m going to cover 3 ways that you can configure CORS to allow for resource sharing between a backend API from NextJS and a frontend.
NextJS CORS Environment Setup
In order to get things up running, we’ll be running two services, NextJS as an API, and a simple Client-Side application that makes HTTP requests to the API.
Initial NextJS Project
We’ll be using the default scaffolded NextJS 13 template.
npx create-next-app@latest nextjs13-cors;
# [Expected Prompts]:
# ✔ Would you like to use TypeScript? … No / Yes
# ✔ Would you like to use ESLint? … No / Yes
# ✔ Would you like to use Tailwind CSS? … No / Yes
# ✔ Would you like to use `src/` directory? … No / Yes
# ✔ Would you like to use App Router? (recommended) … No / Yes
# ✔ Would you like to customize the default import alias? … No / Yes
# Creating a new Next.js app in /path/to/nextjs13-cors.
Creating First NextJS AppRouter Endpoint
We won’t bother with creating a fully functional endpoint, but we’ll create both a GET and POST request.
# FROM: ./
mkdir app/api;
mkdir app/api/users;
touch app/api/users/route.ts;
File: app/api/users/route.ts
// Imports
// ========================================================
import { NextResponse, type NextRequest } from "next/server";
// Endpoints
// ========================================================
/**
* Basic GET Request to simuluate LIST in LCRUD
* @param request
* @returns
*/
export const GET = async (request: NextRequest) => {
// Return Response
return NextResponse.json(
{
data: [
{
id: "45eb616b-7283-4a16-a4e7-2a25acbfdf02",
name: "John Doe",
email: "john.doe@email.com",
createdAt: new Date().toISOString(),
},
],
},
{
status: 200,
}
);
};
/**
* Basic POST Request to simuluate CREATE in LCRUD
* @param request
*/
export const POST = async (request: NextRequest) => {
// Get JSON payload
const data = await request.json();
// Return Response
return NextResponse.json(
{
data,
},
{
status: 200,
}
);
};
Testing NextJS GET & POST Requests
You can use an API Client for this, like Postman, or you can use curl in your terminal.
Let’s run our app.
# FROM: /
pnpm dev;
# [Expected Output]:
# > nextjs13-cors@0.1.0 dev /path/to/nextjs13-cors
# > next dev
#
# - ready started server on [::]:3000, url: http://localhost:3000
Let’s verify that the endpoints work as expected in another Terminal window.
# FROM: /
# TEST 1 - GET Method for NextJS Endpoint
curl --location 'http://localhost:3000/api/users';
# [Expected Output]:
# {"data":[{"id":"45eb616b-7283-4a16-a4e7-2a25acbfdf02","name":"John Doe","email":"john.doe@email.com","createdAt":"2023-08-30T18:17:39.277Z"}]}
# TEST 2- POST Method for NextJS Endpoint
curl --location 'http://localhost:3000/api/users' \
--header 'Content-Type: application/json' \
--data '{
"hello": "there"
}';
# [Expected Output]:
# {"data":{"hello":"there"}}
Creating Client-Side App
Next we want to use another frontend application to make request to our backend API. To get things working quickly without having to configure React or a server, we’re going to use VanillaJS and a package called http-server.
# FROM: ./
pnpm add -D http-server;
We’ll then create a folder called client with an HTML and JavaScript file that makes HTTP requests to our endpoints.
# FROM: /
mkdir client;
touch client/index.html;
touch client/script.js;
In our HTML file, we’ll put the following:
File: client/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Client-Side Tests For NextJS Cors</title>
<script src="/script.js"></script>
</head>
<body>
<main>
<h1>Client-Side Application Testing Cors</h1>
<h2>GET</h2>
<form id="form-get">
<button type="submit">Submit</button>
</form>
<pre
style="
width: 500px;
height: 400px;
background: #efefef;
overflow: scroll;
"
><code id="result-get"></code></pre>
<hr />
<h2>POST</h2>
<form id="form-post">
<div>
<label for="payload" style="display: block; margin: 0px 0px 10px 0px;">JSON Payload</label>
<textarea rows="10" name="payload" id="payload"></textarea>
</div>
<div>
<button type="submit">Submit</button>
</div>
</form>
<pre
style="
width: 500px;
height: 400px;
background: #efefef;
overflow: scroll;
"
><code id="result-post"></code></pre>
</main>
</body>
</html>
Next create the associated JavaScript file.
File: client/script.js
// Config
// ========================================================
const API_URL = "http://localhost:3000/api";
// Functions
// ========================================================
const requests = {
GET: async (callback) => {
const response = await fetch(`${API_URL}/users`);
const data = await response.json();
if (callback) {
callback(data);
}
},
POST: async (payload, callback) => {
const response = await fetch(`${API_URL}/users`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
const data = await response.json();
if (callback) {
callback(data);
}
},
};
// Init
// ========================================================
/**
* When window is loaded
*/
window.onload = () => {
console.group("Window loaded");
// Elements
const formGet = document.getElementById("form-get");
const resultGet = document.getElementById("result-get");
const formPost = document.getElementById("form-post");
const resultPost = document.getElementById("result-post");
// Event Listeners
formGet.addEventListener("submit", (event) => {
event.preventDefault();
requests.GET((data) => {
resultGet.innerHTML = JSON.stringify(data, null, 2);
});
});
formPost.addEventListener("submit", (event) => {
event.preventDefault();
requests.POST(
JSON.parse(event.currentTarget.payload.value),
(data) => {
resultPost.innerHTML = JSON.stringify(data, null, 2);
}
);
});
console.groupEnd();
};
Now we can run our client by modifying our package.json file with a new run command called “client”.
File: package.json
{
"name": "nextjs13-cors",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint",
"client": "http-server -p 3001 ./client"
},
"dependencies": {
"@types/node": "20.5.7",
"@types/react": "18.2.21",
"@types/react-dom": "18.2.7",
"eslint": "8.48.0",
"eslint-config-next": "13.4.19",
"next": "13.4.19",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "5.2.2"
},
"devDependencies": {
"http-server": "^14.1.1"
}
}
Run the new app.
# FROM: /
pnpm client;
# [Expected Output]:
# > nextjs13-cors@0.1.0 client /path/to/nextjs13-cors
# > http-server -p 3001 ./client
#
# Starting up http-server, serving ./client
#
# http-server version: 14.1.1
#
# http-server settings:
# CORS: disabled
# Cache: 3600 seconds
# Connection Timeout: 120 seconds
# Directory Listings: visible
# AutoIndex: visible
# Serve GZIP Files: false
# Serve Brotli Files: false
# Default File Extension: none
#
# Available on:
# http://127.0.0.1:3001
# http://192.168.1.140:3001
# Hit CTRL-C to stop the server
If our NextJS server is still running on port 3000 and then try with our client-side application, we should see that we encounter CORS issues.
Now that we have identified the issue, I’ll walk you through 3 ways to fix CORS issues with NextJS 13 AppRouter.
CORS Fix Option 1 — Next Config
One of the quickest ways to get things fixed is to apply a blanket statement over all endpoints, and this can be achieved through NextJS config file, by setting the right headers.
File: next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
async headers() {
return [
{
// Routes this applies to
source: "/api/(.*)",
// Headers
headers: [
// Allow for specific domains to have access or * for all
{
key: "Access-Control-Allow-Origin",
value: "*",
// DOES NOT WORK
// value: process.env.ALLOWED_ORIGIN,
},
// Allows for specific methods accepted
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
// Allows for specific headers accepted (These are a few standard ones)
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
},
};
module.exports = nextConfig;
Our NextJS Server will prompt us to restart, and if do and go back to our client-side app, we can see that the request is successful.
One of the downfalls for this option is that you can’t dynamically changes values if you want to allow more than one allowed origin. You would basically have to hard code it, as ENV variables are not read.
This means, if you want to update an allowed origin, update to accept a custom header, and/or configure max age or more, you would need to submit a new PR to have your code redeployed.
It is however a great place to get things applied to many routes at once.
CORS Fix Option 2— Route Handler
Option 2 involves creating CORS configurations directly in the route itself. Let’s make sure to comment out our next.config.js
headers and start working with the users route directly.
For this, we’re going to create two environment variables in .env to start.
# FROM: ./
touch .env;
File: .env
ALLOWED_METHODS="GET, POST, PUT, DELETE, OPTIONS"
ALLOWED_ORIGIN="*"
ALLOWED_HEADERS="Content-Type, Authorization"
DOMAIN_URL="http://localhost:3000"
Next we can now modify our route to validate an origin and take those values from our environment variable file. You’ll also notice that in the file, we had to create an explicit OPTIONS request to handle preflight requests.
File: app/api/users/route.ts
// Imports
// ========================================================
import { NextResponse, type NextRequest } from "next/server";
// Config CORS
// ========================================================
/**
*
* @param origin
* @returns
*/
const getCorsHeaders = (origin: string) => {
// Default options
const headers = {
"Access-Control-Allow-Methods": `${process.env.ALLOWED_METHODS}`,
"Access-Control-Allow-Headers": `${process.env.ALLOWED_HEADERS}`,
"Access-Control-Allow-Origin": `${process.env.DOMAIN_URL}`,
};
// If no allowed origin is set to default server origin
if (!process.env.ALLOWED_ORIGIN || !origin) return headers;
// If allowed origin is set, check if origin is in allowed origins
const allowedOrigins = process.env.ALLOWED_ORIGIN.split(",");
// Validate server origin
if (allowedOrigins.includes("*")) {
headers["Access-Control-Allow-Origin"] = "*";
} else if (allowedOrigins.includes(origin)) {
headers["Access-Control-Allow-Origin"] = origin;
}
// Return result
return headers;
};
// Endpoints
// ========================================================
/**
* Basic OPTIONS Request to simuluate OPTIONS preflight request for mutative requests
*/
export const OPTIONS = async (request: NextRequest) => {
// Return Response
return NextResponse.json(
{},
{
status: 200,
headers: getCorsHeaders(request.headers.get("origin") || ""),
}
);
};
/**
* Basic GET Request to simuluate LIST in LCRUD
* @param request
* @returns
*/
export const GET = async (request: NextRequest) => {
// Return Response
return NextResponse.json(
{
data: [
{
id: "45eb616b-7283-4a16-a4e7-2a25acbfdf02",
name: "John Doe",
email: "john.doe@email.com",
createdAt: new Date().toISOString(),
},
],
},
{
status: 200,
headers: getCorsHeaders(request.headers.get("origin") || ""),
}
);
};
/**
* Basic POST Request to simuluate CREATE in LCRUD
* @param request
*/
export const POST = async (request: NextRequest) => {
// Get JSON payload
const data = await request.json();
// Return Response
return NextResponse.json(
{
data,
},
{
status: 200,
headers: getCorsHeaders(request.headers.get("origin") || ""),
}
);
};
We should see the same result, where requests are working as expected with no CORS issues.
The main benefit of this method is that we can be very specific as to which endpoint would get specific CORS configurations, and we can leverage environment variables to switch out the values without deploying hard-coded values.
The main downfall of this option is that it’s siloed and doesn’t provide a wider configuration to apply to many endpoints.
CORS Fix Option 3— Middleware (Recommended)
This method I recommend because it’s the best of Option 1, where it applies to multiple endpoints, and Option 2, where you can configure things further through the use of environment variables.
To start, I’m going to comment out what we did in Option 2, so that we can leverage the middleware.
File: app/api/users/route.ts
// Imports
// ========================================================
import { NextResponse, type NextRequest } from "next/server";
// Config CORS
// ========================================================
// /**
// *
// * @param origin
// * @returns
// */
// const getCorsHeaders = (origin: string) => {
// // Default options
// const headers = {
// "Access-Control-Allow-Methods": `${process.env.ALLOWED_METHODS}`,
// "Access-Control-Allow-Headers": `${process.env.ALLOWED_HEADERS}`,
// "Access-Control-Allow-Origin": `${process.env.DOMAIN_URL}`,
// };
// // If no allowed origin is set to default server origin
// if (!process.env.ALLOWED_ORIGIN || !origin) return headers;
// // If allowed origin is set, check if origin is in allowed origins
// const allowedOrigins = process.env.ALLOWED_ORIGIN.split(",");
// // Validate server origin
// if (allowedOrigins.includes("*")) {
// headers["Access-Control-Allow-Origin"] = "*";
// } else if (allowedOrigins.includes(origin)) {
// headers["Access-Control-Allow-Origin"] = origin;
// }
// // Return result
// return headers;
// };
// Endpoints
// ========================================================
// /**
// * Basic OPTIONS Request to simuluate OPTIONS preflight request for mutative requests
// */
// export const OPTIONS = async (request: NextRequest) => {
// // Return Response
// return NextResponse.json(
// {},
// {
// status: 200,
// headers: getCorsHeaders(request.headers.get("origin") || ""),
// }
// );
// };
/**
* Basic GET Request to simuluate LIST in LCRUD
* @param request
* @returns
*/
export const GET = async (request: NextRequest) => {
// Return Response
return NextResponse.json(
{
data: [
{
id: "45eb616b-7283-4a16-a4e7-2a25acbfdf02",
name: "John Doe",
email: "john.doe@email.com",
createdAt: new Date().toISOString(),
},
],
},
{
status: 200,
// headers: getCorsHeaders(request.headers.get("origin") || ""),
}
);
};
/**
* Basic POST Request to simuluate CREATE in LCRUD
* @param request
*/
export const POST = async (request: NextRequest) => {
// Get JSON payload
const data = await request.json();
// Return Response
return NextResponse.json(
{
data,
},
{
status: 200,
// headers: getCorsHeaders(request.headers.get("origin") || ""),
}
);
};
I’m also going to add some additional CORS configurations to our .env file that would be good to have for further customization later.
File: .env
ALLOWED_METHODS="GET, HEAD, PUT, PATCH, POST, DELETE, OPTIONS"
ALLOWED_ORIGIN="http://localhost:3001,http://localhost:3000" # * for all
ALLOWED_HEADERS="Content-Type, Authorization"
EXPOSED_HEADERS=""
MAX_AGE="86400" # 60 * 60 * 24 = 24 hours
CREDENTIALS="true"
DOMAIN_URL="http://localhost:3000"
Next I’m going to create a new file in the root of the project called middleware.ts
.
# FROM: ./
touch middleware.ts;
File: middleware.ts
// Imports
// ========================================================
import { NextResponse, type NextRequest } from "next/server";
// Config
// ========================================================
const corsOptions: {
allowedMethods: string[];
allowedOrigins: string[];
allowedHeaders: string[];
exposedHeaders: string[];
maxAge?: number;
credentials: boolean;
} = {
allowedMethods: (process.env?.ALLOWED_METHODS || "").split(","),
allowedOrigins: (process.env?.ALLOWED_ORIGIN || "").split(","),
allowedHeaders: (process.env?.ALLOWED_HEADERS || "").split(","),
exposedHeaders: (process.env?.EXPOSED_HEADERS || "").split(","),
maxAge: process.env?.MAX_AGE && parseInt(process.env?.MAX_AGE) || undefined, // 60 * 60 * 24 * 30, // 30 days
credentials: process.env?.CREDENTIALS == "true",
};
// Middleware
// ========================================================
// This function can be marked `async` if using `await` inside
export async function middleware(request: NextRequest) {
// Response
const response = NextResponse.next();
// Allowed origins check
const origin = request.headers.get('origin') ?? '';
if (corsOptions.allowedOrigins.includes('*') || corsOptions.allowedOrigins.includes(origin)) {
response.headers.set('Access-Control-Allow-Origin', origin);
}
// Set default CORS headers
response.headers.set("Access-Control-Allow-Credentials", corsOptions.credentials.toString());
response.headers.set("Access-Control-Allow-Methods", corsOptions.allowedMethods.join(","));
response.headers.set("Access-Control-Allow-Headers", corsOptions.allowedHeaders.join(","));
response.headers.set("Access-Control-Expose-Headers", corsOptions.exposedHeaders.join(","));
response.headers.set("Access-Control-Max-Age", corsOptions.maxAge?.toString() ?? "");
// Return
return response;
}
// See "Matching Paths" below to learn more
export const config = {
matcher: "/api/:path*",
};
Now if we try our client-side application again with the updated environment variables, we can see that things work as expected for GET and POST HTTP requests.
Full Code Repository
If you want to see all these implementations within a single repository, here is the full repository link to NextJS13 CORS.
What’s Next?
The next step would be to get off localhost and get this code deployed through Vercel to see it working and understand how to create APIs that allow Client-Side Applications to interact with it.
If you got value from this, please give it some love, and please also follow me on X / Twitter (where I’m quite active) @codingwithmanny and Youtube at @codingwithmanny.