Does NextJS make CORS and API keys handling easier?

NextJS is primarily meant for front-end developers; NextJS also supports back-end development; CORS is a security feature enforced by browser on the client side (front-end). And this led to some other findings.

33 views
TechCode
A man working on his laptop with a bunch of keys hanging behind him in the wall

A man working on his laptop with a bunch of keys hanging behind him in the wall

TL;DR: This article isn’t a straightforward technical guide on anything but a story written to share simple things we, as a developer, face on day to day basis. It walks through how a CORS issue led to exploring server-side proxies, securely handling API keys, and understanding how NEXT_PUBLIC environment variables work in NextJS.

As always, it starts with a recent project that I was working on; when trying to call a third-party API (let’s say ExternalXYZAPI) from the client side, the request was blocked due to CORS issues. Our team was actually expecting the ExternalXYZAPI team to allow CORS on the production domain, so we developed, tested, gave a demonstration to stakeholders, and even previewed the staging deployment in a browser with web security disabled.
Later, only after this implementation was pushed to production, we were informed by one of the members from the ExternalXYZAPI team that they won’t allow client (browser) to directly request the API.
And, based on their suggestion, we were pushed to look into this server proxy technique.

So, let’s start with what “CORS” and “server proxy” really are and how they led to other important findings.

What is CORS?

CORS, or Cross-Origin Resource Sharing, is a security feature implemented by web browsers to prevent malicious websites from making requests to a different domain (origin) than the one the user is currently visiting. When a web app tries to access an API or resources hosted on a different domain, the browser checks if the API allows such requests by inspecting the CORS headers sent by the server. If the server doesn’t send the right CORS headers, the browser blocks the request for security reasons.

Here’s how it works:
1. Request is sent: The client-side application (e.g., a web browser) tries to make an HTTP request to an external API (server) that’s hosted on a different domain than the one the client is currently on.
2. Server responds: The external API server processes the request and sends back the response data along with the CORS headers (or without them, if it doesn’t support cross-origin requests).

Let’s say if the server at externalxyzapi.com allows cross-origin requests from the domain https://your-frontend.com, the server would include the following header in the response:

Access-Control-Allow-Origin: https://your-frontend.com

3. Browser checks headers: The browser reads the Access-Control-Allow-Origin header in the server’s response. If the value matches the client’s origin (in our case https://your-frontend.com), the CORS headers are valid.
4. Data access:
• If the CORS headers are valid, the browser allows the JavaScript running on the client to access the data in the response (for example, to display the data on the page).
• If the origin does not match, the browser blocks access to the response data and logs a CORS error in the console.

What is a Proxy?

In general, a proxy is any intermediary that acts as a bridge between two parties to facilitate communication, handle requests, or enforce rules. It can be a person, server, or application that operates on behalf of someone or something else.

Everyday Examples:
• Person as a Proxy: If someone negotiates on your behalf, they are acting as your proxy.
• Server as a Proxy: A proxy server forwards or modifies requests between a client (like a browser) and a destination (like an external API).

So, in general, the proxy doesn’t originate the request or response itself but mediates between the actual sender and receiver. And, in our case, we are talking about the second example.

A server-side proxy is a server that acts as an intermediary between the client and another server (in this case, the external API server). Instead of the client communicating directly with the API server, the client communicates with the proxy, and the proxy forwards the request to the API server. When the proxy receives the response from the API server, it sends it back to the client.

Finding 1: Resolving CORS Issues Using Server Proxy

Although I knew what CORS was and that NextJS has a dual nature — works for client-side as well as server-side — I still call it a finding because the term “server-side proxy” (maybe, as a front-end developer, this was the consequence of avoiding every technical term that has “server” in it :D) was not very familiar to me. Also, due to the dependency on the ExternalXYZAPI team(thinking that they will allow CORS on production domain), lack of encounters with these kinds of issues, or maybe a lack of clear understanding (i.e., I couldn’t even link it with CORS at that time) — I didn’t think of going this way (even though I had read an article about server proxies right at the time) until the developer from ExternalXYZAPI suggested it.

Since CORS only applies to client-side requests made from the browser — I repeat — it’s a security measure to ensure the browser checks the origin of the request and only allows access if the server permits it. So, when requests are sent from the server, there’s no browser involved to enforce CORS, and therefore the issue doesn’t arise at all.

Also, NextJS handles both client and server-side logic, allowing front-end developers to easily set up a basic server-side proxy using its API routes feature which also eliminated the need to request a backend developer or set up a separate, complex backend project.

Before (Client-Side Request Causing CORS Issue):

const fetchData = async () => {
const response = await fetch(‘https://third-party-api.com/endpoint', {
method: ‘POST’,
headers: {
Authorization: ‘Bearer API_KEY’,
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify({ data: ‘example’ }),
});
const result = await response.json();
console.log(result);
};

After (Server-Side Proxy in Next.js API Route):

// /pages/api/proxy.js
export default async function handler(req, res) {
const response = await fetch(‘https://third-party-api.com/endpoint', {
method: ‘POST’,
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify(req.body),
});
const result = await response.json();
res.status(response.status).json(result);
}

Client-side request to the proxy:


const fetchData = async () => {
const response = await fetch(‘/api/proxy’, {
method: ‘POST’,
headers: {
‘Content-Type’: ‘application/json’,
},
body: JSON.stringify({ data: ‘example’ }),
});const result = await response.json();
console.log(result);
};

This setup resolved the CORS issue by making the client communicate only with our server, and the server handling communication with the external API.

Finding 2: Securing API Keys: Protecting Sensitive Data

During the process, I realized that I was exposing the API key in the request header while requesting the APIs from the client-side. This could be seen in the browser’s network requests. So, when switched to the proxy server implementation, the API key was only accessible on the server side, keeping it safe from the client.

Finally, CORS issue was resolved and with the proxy server there was no fear of exposing API keys in the request header any more. I created a PR (FYI, it was my third PR related to same feature) and with some additional works based on reviewer’s suggestion it was approved and then deployed to production.

Everything’s good now.

Wait! No, not yet.

Finding 3: Environment Variables: A Clear Separation Between Server and Client

During the process, I also gained a clear understanding of how environment variables work under the hood in NextJS to make it easy to work with either server-side or client-side code.
It’s known that, in NextJS, environment variables must be prefixed with NEXT_PUBLIC_ in order to be used in client-side code. Within the framework, what NextJs actually does to these type of variable is — these variables will be “inlined” at build time into the JS bundle that is delivered to the client, replacing all references to process.env.<NEXT_PUBLIC_-prefixed variable> with a hard-coded value.

In our case, during the initial implementation of the client-side API integration, I used an environment variable for the API key (say NEXT_PUBLIC_XYZ_API_KEY) with the NEXT_PUBLIC_ prefix to make it accessible on the client side. Later, while changing the implementation to the server-side proxy, this same variable was reused (mistakenly). The setup worked correctly, and the variable wasn’t exposed in network requests (like headers). However, according to NextJS documentation, NEXT_PUBLIC_-prefixed variables would still be exposed in the JS bundle, created during the build process, and sent to the client. Yes, this thing was again, already happening on the production!

Next, I created a PR once again removing the prefix NEXT_PUBLIC_ used for the key, thanked the reviewers for bearing with me as I kept coming back to the same feature again and again :D, rotated the API key (updated the Vercel environment variable with a new key), and finally deployed the changes.

So, this is how, what began as a CORS issue led me to better understand how NextJS helps front-end developers handle server-side proxying and securely manage API keys.

Discussion

Coming Soon!