Circumventing CORS with Netlify Functions & Nodejs

Kamry Bowman
Apr 19 · 7 min read
Photo by BENCE BOROS on Unsplash

Recently I was working on a front end project, where I needed to consume a third-party API that hadn’t been properly configured with the appropriate CORS headers. Rather than track down the API author, I decided to take this as an opportunity to experiment with a serverless pattern. As someone who usually reaches for a library to assist with http requests, I figured it was also an opportunity to practice making those requests with just the nodejs standard libraries.

Why a custom proxy?

CORS limit websites from communicating with other domains without the full consent of both sites. Oftentimes it only makes sense for a a specific client to use a server, but other times a server can have an API that is useful for various sites. However, if the server is not set up properly, browsers will prevent other websites from using that API. A proxy server using serverless function can be set up so your Netlify site is consuming data from your Netlify Functions’ endpoints, not the cross-origin site. Problem solved.

Note: Circumventing CORS should be undertaken with care. But a proxy server still preserves a major protection of CORS, as discussed here.

Why serverless?

Serverless is a popular pattern that has been driven by the popularity of AWS Lambda. What’s the pattern? The idea is you don’t have your own server, not even a dyno like the case with Heroku. Rather, you just have a single function call that takes a http request and returns a response. Simple, clean. No need to worry about server down-time, or pinging your server to make sure it’s up. Also, scaling questions are handled by the provider of your serverless service, your functions are stateless and called by the surrounding infrastructure. Less state is easier to reason about.

Why Netlify Functions?

Netlify Functions is a user-friendly wrapper around AWS Lambda. So when you use it, you are getting a taste of AWS Lambda, an industry standard. You even will find yourself reading AWS Lambda docs at times, which is a great way to start getting familiar with an API: having a specific question to answer in the docs.

On the other hand, Netlify aims for a friendly experience. So a lot of the “overhead” of AWS is abstracted away. Everything just works with Netlify.

Netlify also provides the netlify-lambda library, which lets you run your lambda functions locally, which makes them easy to test. Netlify is also rolling out the Netlify Dev platform, which will make this local testing environment even easier to interact with!

Zeit is another strong contender in the serverless space. I first explored their options but did not find the local testing options as robust. But Zeit moves fast and I expect that to no longer be true soon, if it isn’t already.

Why node?

Node is the ultimate async scripting language, so for a quick easy solution nothing beats it for me. But why basic node, why not a http request library? Because sometimes libraries abstract away so much that you can lose track of what exactly is happening under the hood. Sometimes you think there’s more magic in the library than there is, and you can become disempowered to solve problems. I wanted to counteract that trend by keeping things basic and light.

The approach

The API was a beer-rating app. It had an endpoint for creating a beer, updating the number of likes a beer had, and fetching a list of beers. I already had a Create-React-App based project structure, so to get started, I did yarn install netlify-lambda. Then I created an api folder in my src folder, and created a beer.js file (so /src/api/beer.js). This would mean my front-end could query /api/beer to interact with the third-party API.

I then created a netlify.toml file in my root folder that ended up looking like this:

[[redirects]]
from = "/*"
to = "/index.html"
status = 200

[build]
command = "yarn build"
functions = "api"
publish = "build"

Everything above is unrelated to Netlify Functions, except the functions line, which is telling Netlify where to look for the endpoint files. You could also handle this on the Functions settings page for your Netlify site.

Finally, I went to the Functions tab on my Netlify account for my site and enabled Functions.

Node Code

First we import our libraries and set our baseURL (not the real url):

const https = require('https');
const url = require('url');

const baseUrl = https://beer-rater-api.com // not the actual site address

What’s going on here? I’m importing the https and the url node standard libraries. https can be used to implement a full-fledged server, but in this case we're using it only to make http requests to the third-party API-similar to what fetch would be used for in the browser. And url is used to parse urls. For latest versions of node, url would not be needed to accomplish our goals, but for the version of node supported out of the box with Netlify, we need it to get https to work for us.

That done, we set up our central endpoint, which handles routing for our request.

function getBeers(callback) {
const clientReq = https.request(
{
...url.parse(baseUrl),
headers: {
'Content-Type': 'application/json',
},
},
clientRes => {
clientRes.setEncoding('utf8');
let rawData = '';
clientRes.on('data', chunk => {
rawData += chunk;
});
clientRes.on('error', err => {
console.log('error here line 140', err);
callback(err.message);
});
clientRes.on('end', () => {
callback(null, {
statusCode: 200,
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: rawData,
});
});
}
);
clientReq.end();
}

We are exporting a function that takes as arguments, event, context, and a callback. These are all provided by AWS, so you will want to check out their docs rather than Netlify's.

The event argument has info about the server request, and the callback function is used to resolve the request. It takes two parameters, Error, and Response. So an error response can be triggered with callback("An error occurred."), and a success can be triggered with callback(null, "success message!").

The function uses regex to get any routing off the received url, and then uses the http method information and the route to determine the correct course of action. I’ll walk through the functions associated with the different actions.

GET Requests

The GET request is the simplest kind, so we handle it first. We use the standard event listener approach to making our http request. One thing to note, is that https.request takes a url object as its argument, so we need to parse our endpoint using url.parse. We then spread it into an object so we can add headers as needed.

function getBeers(callback) {
const clientReq = https.request(
{
...url.parse(baseUrl),
headers: {
'Content-Type': 'application/json',
},
},
clientRes => {
clientRes.setEncoding('utf8');
let rawData = '';
clientRes.on('data', chunk => {
rawData += chunk;
});
clientRes.on('error', err => {
console.log('error here line 140', err);
callback(err.message);
});
clientRes.on('end', () => {
callback(null, {
statusCode: 200,
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: rawData,
});
});
}
);
clientReq.end();
}

Preflight Requests: OPTIONS, PUT, and POST

I ran into trouble when trying to make PUT and POST requests, because these require a preflight request to be sent and responded to beforehand. That is why the main function above first checks to see if a request is using the OPTIONS method, and handles it if needed. This is an aspect of code that is frequently abstracted away, such as in the Express framework. So I found it instructive to have to handle this myself.

The preflight response is fairly straightforward. Just send the headers back that you will be sending on your eventual POST/PUT response.

function preflight(callback) {
callback(null, {
statusCode: 204,
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT',
},
body: {},
});
}

After that, the POST and PUT requests are fairly similar, so I present them together:

function updateLikes(id, likes, callback) {
const clientReq = https.request(
{
...url.parse(`baseUrl/${id}`),
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
},
clientRes => {
clientRes.setEncoding('utf8');
clientRes.on('error', err => {
callback(err.message);
});
clientRes.on('data', () => {
// data is not needed, but res requires
// data event to be processed to exit
});
clientRes.on('end', () => {
try {
callback(null, {
statusCode: 204,
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
} catch (e) {
console.log(e, 'caught');
callback(e.message);
}
});
}
);
clientReq.write(
JSON.stringify({
likes,
})
);
clientReq.end();
}

function createBeer(name, likes, callback) {
const clientReq = https.request(
{
...url.parse(baseUrl),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
},
clientRes => {
clientRes.setEncoding('utf8');
clientRes.on('error', err => {
console.log(err);
callback(err.message);
});
clientRes.on('data', () => {});
clientRes.on('end', () => {
try {
callback(null, {
statusCode: 204,
headers: {
'content-type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
});
} catch (e) {
console.log(e, 'caught');
callback(e.message);
}
});
}
);
clientReq.write(
JSON.stringify({
name,
likes,
})
);
clientReq.end();
}

And that’s essentially it! Notice, we are adding an ‘Access-Control-Allow-Origin’: ‘*’ header to our responses. In reality, this is not required, and could be gotten rid of in a production app. The whole point of using Netlify Functions is that this API will be served from our own app’s netlify domain, so CORS issues will be avoided! But including the headers does allow you to support other clients as well. Note that in many cases, you would want to specify allowable urls rather than using *.

Consuming the endpoints

Since you’ve installed netlify-lambda, you can now run yarn netlify-lambda to run your function. By default, it will run on localhost:9000. You can now make http requests against it using CURL, Postman, or vscode REST Client.

To access it in your front-end, you can use environmental variables, so that your front-end hits http://localhost:9000 in local development, and./.netlify/functions in production.

Next steps

And with that, we’ve implemented a basic endpoint with routing and multiple method support. Some additional concepts you could explore would be setting up your netlify-lambda so it uses a proxy to support requests to ./.netlify/functions in the local environment as well as production, to keep differences to a minimum.

You can also start exploring the Netlify Dev package, as this will only make this technology easier to implement and test.

Or you could run with the big dogs, and implement this directly using AWS!


Originally published at https://glitteringglobofwisdom.com on April 19, 2019.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade