logo My Digital Garden

NestJS 13 series - Adding Cors

By James Kolean on Nov 27, 2023
Source repository: https://gitlab.com/jameskolean/next-explore
Demo: https://next-explore-liart.vercel.app/
JavaScriptNext
banner

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.

Set up a tester

  1. Add a simple HTML server
pnpm add -D http-server
  1. Add a page and some javascript to test with
<!-- 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();
};
  1. Add a script to package.json to run out tester
    "client": "http-server -p 3001 ./client"
  1. Run the tester pnpm client

Middleware

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"

Summary

Run the tester app with and without the CORS middleware.

© Copyright 2023 Digital Garden cultivated by James Kolean.