Skip to main content

Node.js/Express CORS implementation

7 min read

Older Article

This article was published 12 years ago. Some information may be outdated or no longer applicable.

I’ve been doing quite a lot of work with Node.js and Express recently, for a variety of reasons I won’t get into now.

Long story short, I’ve worked with various REST APIs and in my spare time read an excellent book: A Practical Approach to API Design. The book covers good practices for creating REST APIs. If you’re building APIs, give it a read.

While running tests, I kept hitting two issues:

  • Access-Control-Allow-Origin related errors
  • 401 Unauthorised type messages

The reason was simple. I was testing a REST API running on port 3000 while my frontend was running on port 8000. The Access-Control-Allow-Origin error fired in that case. The 401 messages appeared when I was testing remote APIs and didn’t realise they required an authentication token. I decided to dig into CORS properly, and that led me to building my own Node.js/Express REST server that accepts authentication tokens and handles CORS.

First, understand that Access-Control-Allow-Origin is a setting on the server side, not the client side. If the API provider hasn’t set Access-Control-Allow-Origin to accept your domain, your HTTP GET requests will always fail. In that case, we can fall back to JSONP. Here’s a simplified AngularJS example:

function($scope, $http) {
  // This would be a standard GET request to an API
  $http.get('/path/to/api/service').success(function (data) {
    console.log(data);
  });

  // However the above could easily fail with Access-Control-Allow-Origin error. In that case we can always try:
  $http.jsonp('/path/to/api/service?callback=JSON_CALLBACK').success(function (data) {
    console.log(data);
  });
}

Per the AngularJS documentation:

Relative or absolute URL specifying the destination of the request. The name of the callback should be the string JSON_CALLBACK. https://docs.angularjs.org/api/ng/service/$http#jsonp

The callback’s name is outside our control. So what if the callback is actually named “rest_callback”? You can use this workaround to still grab the data (not the prettiest solution, but it works):

window.rest_callback = function (data) {
  console.log(data);
};

Let’s get to it and write a Node.js/Express application that serves some data. I’m using Express 4 along with its new routing options. These lines should be self-explanatory:

var express = require('express');
var morgan = require('morgan');
var bodyParser = require('body-parser');
var methodOverride = require('method-override');
var app = express();
var router = express.Router();

app.set('port', process.env.PORT || 3000);

app.listen(app.get('port'), function () {
  console.log('Express up and listening on port ' + app.get('port'));
});

We set up the required variables and fire up Express on the node’s port (if defined) or on port 3000.

Let’s extend this by adding a route for root:

app.route('/').get(function (req, res) {
  res.send('hello');
});

Navigate to localhost:3000 (or whatever IP/hostname/port you’re using) and you should see “Hello” appearing. Now change res.send('hello'); to res.json({data: 'Hello'}); to return a JSON object in the response.

Let’s throw AngularJS into the mix. We’ll create a very simple application and try to consume the (extremely complex) API we’ve just built:

<html ng-app="myapp">
  <head>
    <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.24/angular.min.js"></script>
    <script>
      var app = angular.module('myapp', []);

      app.controller('MyCtrl', function ($scope, $http) {
        $scope.getData = function () {
          $http
            .get('http://localhost:3000/')
            .success(function (data, status) {
              console.log('Status: ', status);
              console.log('Data: ', data);
              $scope.serverData = data.data;
            })
            .error(function (data, status) {
              console.log('Status: ', status);
              console.log('Data: ', data || 'Request failed');
            });
        };
      });
    </script>
  </head>
  <body>
    <div ng-controller="MyCtrl">
      <p>{{ serverData }}</p>
      <p><button ng-click="getData()">Get data!</button></p>
    </div>
  </body>
</html>

All we have is a button that calls getData() from our MyCtrl controller. It tries to pull data from the REST endpoint running at localhost:3000. The result, unfortunately, is an error.

Requests fail in these scenarios:

  • Same protocol and host, but different port (like ours)
  • Different protocol (http vs https)
  • Different host (api.steve.com vs api.dave.com)
  • Same host but different format (example.com vs www.example.com)
  • Same host but with subdomain (example.com vs me.example.com)

Requests don’t fail if the hosts are exactly the same. So example.com matches example.com/page/1.

What we need to do is tell our server to allow our host to communicate with the backend. We do that by adding the following to our Express server:

router.use(function (req, res, next) {
  res.header('Access-Control-Allow-Origin', 'http://localhost:8000');
  next();
});

This is an Express Router middleware. Re-run the previous example and you should see “Hello World” in the browser.

For fun, try changing the Access-Control-Allow-Origin value and watch the requests break.

Now let’s add authentication to our REST API. We want to send a special header from our AngularJS application to the server with an authentication key. Add the following to the AngularJS code:

var config = {
  headers: {
    'X-Auth-Key': 'abc123',
  },
};
// also update the $http.get() line:
// $http.get('http://localhost:3000/', config).success(... etc

This tells the $http service to send a config object with an X-Auth-Key header. Run the AngularJS application and try to fetch data from the server. You’ll find you can’t. Another XMLHttpRequest error.

Should be easy to fix. Just add another option to our Router middleware, right? Let’s see what happens when we update our Express app:

router.use(function (req, res, next) {
  res.header('Access-Control-Allow-Origin', 'http://localhost:8000');
  // we have added this Access-Control-Allow-Headers option
  res.header('Access-Control-Allow-Headers', 'X-Auth-Key');
  next();
});

Go back to the application and hit the “Get data!” button. Nothing. You’ll see “clicked” in the console but nothing else obvious. Check the “Network” tab and you’ll spot the GET request sitting in a pending state, stuck there forever.

But notice there’s been a second request using the “OPTIONS” method. Turns out browsers send a “preflight” request to the server using the HTTP verb OPTIONS. Once the server approves, the actual HTTP request (our GET) can fire.

Why is our GET pending? After fiddling with the code, I found the issue. Our Express router handles the GET HTTP verb but not OPTIONS. The first request goes through, but the next one just hangs. Let’s extend the router:

app
  .route('/')
  .options(function (req, res, next) {
    res.status(200).end();
    next();
  })
  .get(function (req, res) {
    res.json({ data: 'Hello World' });
  });

We accept the OPTIONS HTTP request and send back a status of 200 (there’s some debate on forums about whether the response should be 200 or 204; I haven’t investigated that yet, but 200 works fine for now). Then next() lets us iterate through the various middlewares set up in our application. Note that route order matters. Defining them the other way around (GET first, OPTIONS second) won’t work unless you add next() calls accordingly.

Re-run the example and the data should appear again. Check the network tab, open the second request, and you’ll see the custom header.

Let’s use this authentication token to control the data we return. Here’s the final route setup:

app
  .route('/')
  .options(function (req, res, next) {
    res.status(200).end();
    next();
  })
  .get(function (req, res) {
    // notice how the key is all lowercase!
    var clientKey = req.headers['x-auth-key'];
    var acceptedKey = 'abc123';
    if (clientKey !== acceptedKey) {
      res.status(401).end();
    } else {
      res.json({ data: 'Hello World' });
    }
  });

Run the example with the right authentication token from AngularJS and the data appears. Change the config variable to a different token and you’ll see a 401 Unauthorised error:

var config = {
  headers: {
    // update this to have a value of abc111 for example
    'X-Auth-Key': 'abc123',
  },
};

That’s it. The key takeaway: set up your Express middlewares correctly and handle the OPTIONS HTTP verb for your routes.