Setting up multiple subdomains on one express app in Heroku

Let's say you've decided to create a website; before even starting to build it, you'll probably find yourself brainstorming what it should be called. So one of the first things that you do is look for and purchase a domain name. Some folks believe that you gotta "lock in" that domain name before you even write one line of code. Fair enough, I can understand and somewhat agree with that. After some searching, you decide to purchase googly.com.

After several months, you start getting new ideas. These new ideas are still related to googly.com, but they are disconnected from the main intention of your original website. As a result, you decide to create subdomains, where each subdomain is a different web app or experience that are still under the same umbrella of your domain name. So you decide to register blog.googly.com, api.googly.com, news.googly.com, etc into your DNS config. Now that you've registered all these subdomains, you'll have to point each of them to a respective app and/or machine. This is where things get a little more interesting.

Multiple apps are expensive

The easiest, purist, and most scalable approach would be to dedicate a brand new machine and codebase for each new subdomain. For example, you would have a dedicated repo for blog.googly.com, it would deploy to its own machine (and domain/IP), and you would create a CNAME record from your subdomain to that machine. Easy-peezy. This is the ideal solution because if you were larger organization, you would have a dedicated team that would only need to interact with this codebase, it would have its own build system, and it would have its own deployment strategy. If this app received more traffic than your other ones, you can set-up a reverse-proxy and provision the appropriate resources to scale as needed. The only problem with this approach is that it takes longer to set up and most importantly, it's expensive.

If the app is small, if it's a hobby app, if you're the sole developer on this project, or if you don't get much traffic... this is extremely overkill. Also if you're a cheap-ass like me or don't have the money to pay for that many machines, this gets very expensive, very fast. So how do you keep costs low while continuing to build all these separate apps? The answer is actually simple - you just put everything in one codebase and deploy it as one individual app.

Whoa whoa, but that goes against what everyone says NOT to do. Yes that is true - we're now essentially going to be building a monolithic codebase and storing all of our code under the same repo. But let's be honest, every major project has started out as a monolith, so unless you are dealing with millions of users, working with hundreds of developers, or your projects aren't related to scaling your architecture, then you'll be fine.

One ring app to rule them all

Let's get started - I will show you how you can create multiple app experiences under different subdomains using the same codebase and deployed as one individual app. For the purposes of this exercise, I will be deploying this on Heroku (again I'm a cheapass and they have a sweet $7 dyno instance). I will also build off the same express codebase that I discussed in my Better Expression Routing blog post. If you haven't checked that out yet, I recommend giving that a quick read and downloading the codebase there so that it'll be easier to follow along. You can download the codebase here.

Setting up your express routes

For the most part you are going to keep your express server setup relatively the same, but we're going to make a few minor tweaks. The first is that we'll want to take advantage of the express-subdomain middleware. This project will enable express to understand and route any subdomains on the domain. Using the express-env example codebase, instead of routing all API based requests through the /api relative path, we're going to create an api subdomain instead.

Before: /server/routes/index.js

const
    apiRoute = require('./apis'),
    homeRoute = require('./home'),
    errorRoute = require('./error');

function init(server) {
    server.get('/', function (req, res) {
        res.redirect('/home');
    });

    server.use('/api', apiRoute);
    server.use('/home', homeRoute);
    server.use('/error', errorRoute);
    
    server.get('*', function (req, res, next) {
        console.log('Request was made to: ' + req.originalUrl);
        return next();
    });
}

module.exports = {
    init: init
};

After: /server/routes/index.js

const
    subdomain = require('express-subdomain'),
    apiRoute = require('./apis'),
    homeRoute = require('./home'),
    errorRoute = require('./error');

function init(server) {
    // Register subdomains first so that the following server rules don't apply
    // to the subdomain as well
    server.use(subdomain('api', apiRoute))

    server.get('/', function (req, res) {
        res.redirect('/home');
    });

    server.use('/home', homeRoute);
    server.use('/error', errorRoute);
    
    server.get('*', function (req, res, next) {
        console.log('Request was made to: ' + req.originalUrl);
        return next();
    });
}

module.exports = {
    init: init
};

For sanity, we're also going to update the /server/routes/apis/index.js to return a 404 for unrecognized endpoints. You can add the following line to the file.

...
router.use('/v1', v1ApiController);
router.use('/v2', v2ApiController);

router.get('*', function (req, res, next) {
  res.status(404).send('ERROR')
});

module.exports = router;

IMPORTANT: When developing locally, you can't attach subdomains to localhost, so you'll also need to add to add new hosts file entries on your machine that point to your localhost and ensure your subdomains are routable. You can follow these instructions on how to do that. But it should look something like:

127.0.0.1       localhost.[name]
127.0.0.1       api.localhost.[name]

Where [name] is your own custom name. If you now try to run the app locally, all your API endpoints should now be available as part of api.localhost.[name].  Note: Do not use dev for the [name] because as of Chrome 63, Chrome will force .dev domains to HTTPS via preloaded HSTS.

Deploying to Heroku

Now that you've got the app running locally, let's go over how to deploy this on Heroku. I'm not going to go over how to deploy a Nodejs app on Heroku, as there are plenty of guides and tutorials that already show you how to do this. Instead, I will go over the nuanced configurations you need for subdomain support.

Make sure you have a Procfile set up so that Heroku nows how to run your app. If you're unsure how that works, you can read more about that here, but it should look something like.

web: node dist/index.js

When my code is built, the main index.js that starts my express server is within the dist/index.js directory, hence why my Procfile starts node against that directory.

Once you've deployed your app on Heroku, open up the settings of your Heroku app and make sure to add NODE_ENV config variable as production. This may not be necessary, but a lot of projects and dependencies rely on the NODE_ENV variable to be set in order to determine how to build/run certain processes, so it doesn't hurt to have this set for sanity reasons.

Next you'll want to set up custom domains for your app. As mentioned earlier, you may have already bought your domain name and want to create your subdomains against that root domain. If so, you can follow the instructions here on how to set them up for your Heroku app. Since we created the api subdomain earlier, that's what we'll add and register to Heroku and your DNS provider.

So adding your domains to Heroku should like the following:

Then you should to go your DNS provider to set up the subdomains like the following:

$ave dat money

And that's it - your api subdomain should now be live! The best part about this setup is that implementing additional subdomains should be just as easy as adding a new subdomain entry within your express app, then registering it in your Heroku and your DNS provider. You don't need to stress about provisioning new machines and paying out of the ass for each one. Unless you got deep pockets or a shit-ton of users, this should keep you going for quite some time.