If you tries to access our APIs from another Web App you will find it is not possible due to CORS. Let’s fix that.
There are several ways we can do this but we will use Middleware. You can also implement the same functionality each Route if you want to enable individual endpoints.
pnpm add -D http-server
<!-- 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>
// client/script.js
// Config
// ========================================================
const API_URL = "http://localhost:3000/api";
// Functions
// ========================================================
const requests = {
GET: async (callback) => {
const response = await fetch(`${API_URL}/greet`);
const data = await response.json();
if (callback) {
callback(data);
}
},
POST: async (payload, callback) => {
const response = await fetch(`${API_URL}/greet`, {
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();
};
package.json
to run out tester "client": "http-server -p 3001 ./client"
pnpm client
Create a new middleware file.
// src/middlewares/withCors.ts
import {
NextFetchEvent,
NextRequest,
NextResponse
} from "next/server";
import {
MiddlewareFactory
} from "./middlewareFactory";
// 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",
};
export const withCors: MiddlewareFactory = (next) => {
return async (request: NextRequest, _next: NextFetchEvent) => {
// Response
const response = NextResponse.next();
if (!request.nextUrl.pathname.startsWith("/api/")) {
return response;
}
// 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;
};
};
Add this middleware to the end of of out chain in src/middleware.ts
import {
middlewareChain
} from "@/middlewares/MiddlewareChain";
import {
withLogger
} from "@/middlewares/withLogger";
import {
withInternalization
} from "@/middlewares/withInternationalization";
import {
withSession
} from "@/middlewares/withSession";
import {
withCors
} from "@/middlewares/withCors";
const middlewares = [withLogger, withInternalization, withSession, withCors];
export default middlewareChain(middlewares);
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - _next/assets (asset files)
* - favicon.ico (favicon file)
*/
"/((?!_next/static|_next/image|assets|favicon.ico|sw.js).*)",
],
};
You will need to add some environmental variables.
# CORS
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"
Run the tester app with and without the CORS middleware.