[Tutorial] How to use subdomains in Express.js with vhost

This quick tutorial shows how to use subdomains with Express.js. If you don’t know Express.js yet, check out some of the previous tutorials, like the introduction to Node.js/Express servers tutorial.

Create a simple Express.js app

Create a new project folder and create two files: package.json and app.js.

The package.json file should look something like this:

    "name": "@j127/express_subdomains",
    "version": "0.1.0",
    "description": "A simple example of using subdomains with Express.js",
    "scripts": {
        "start": "npx nodemon app.js",
        "format": "prettier \"**/*.{js,css,html,json,ts}\" --write"
    "author": "your name",
    "license": "BSD-3-Clause"

The app.js file should look like this to start:

const express = require("express");

const app = express();

app.get("/", (req, res) => {
    res.send("hello world");

const PORT = 3000;
app.listen(PORT, () => console.log(`server is running at${PORT}`));

Install the dependencies

This will install the dependencies and save them:

$ npm install --save express vhost
$ npm install --save-dev nodemon prettier

Formatting your code

For this tutorial, I’ve added prettier as a dependency to help people automatically keep their code neat. Put a file named .prettierrc in your project with these contents:

    "endOfLine": "lf",
    "semi": true,
    "singleQuote": false,
    "tabWidth": 4,
    "trailingComma": "es5"

Also create a file named .prettierignore with these contents:


From this point on, you’ll be able to type one command to format all the code perfectly:

$ npm run format

Those files are in all of my sample repos, so if you’re wondering what they do, that’s basically it, except that my editor formats the code automatically. :slight_smile:

Edit your hosts file

Next step: to view your server with temporary host names, you’ll need to set up your hosts file so that you can access your local server by some custom domain name.

I’m using Linux, and my hosts file is located at /etc/hosts. If you’re using Mac or Windows, find your hosts file with these instructions.

You’ll probably need to use sudo to edit the /etc/hosts file. You can type something like this to edit it on Linux:

$ sudo nano /etc/hosts

Add lines for your local (made-up) domain name and subdomains that point to your local machine ( like this:	mysite.local	cats.mysite.local	dogs.mysite.local

If your server is running on port 3000, then you will be able to visit any of these to view your local development site:

  • mysite.local:3000
  • cats.mysite.local:3000
  • dogs.mysite.local:3000

The names above are arbitrary — you can use whatever names you want in development.

Check that it’s working

The app should now run if you start it like this:

$ npm start

Visit these URLs to check, and the content should be the same for all of them:

  • localhost:3000
  • mysite.local:3000
  • cats.mysite.local:3000
  • dogs.mysite.local:3000

Add subdomain routing

Here’s the app.js file with working subdomains. It isn’t in the final form yet, but I kept it as simple as possible (in one file) for a first look. Read the comments and code to see what it does.

// Import the two dependencies
const express = require("express");
const vhost = require("vhost");

// Create an app for the top-level domain
const app = express();

// Create an app for each subdomain. You can call these whatever you want.
const cats = express();
const dogs = express();

// Set the domain based on whether it's in production or not. If the
// syntax doesn't look familiar, look up "ternary operator javascript"
// in a search engine or leave a comment below.
const domain =
    process.NODE_ENV === "production" ? "example.com" : "mysite.local";

// Mount the extra apps on their subdomains.
app.use(vhost(`cats.${domain}`, cats));
app.use(vhost(`dogs.${domain}`, dogs));

// The routers will be moved to their own files in another step.

// a router for the root domain
app.get("/", (req, res) => {
    res.send(`hello world`);

// a router for the cats subdomain
cats.get("/", (req, res) => {
    res.send("here is the cats subdomain");

// a router for the dogs subdomain
dogs.get("/", (req, res) => {
    res.send("here is the dogs subdomain");

// start the server
const PORT = 3000;
app.listen(PORT, () => console.log(`server is running at${PORT}`));

Check the URLs to see the changes:

  • mysite.local:3000
  • cats.mysite.local:3000
  • dogs.mysite.local:3000

Larger Example

I wrote a quick, larger example and put it on Github. I wrote this tutorial and all the code in two hours, so it’s rough and there are better ways to do some of the things, but it at least shows how to begin splitting things up into their own files. Leave a comment below, or make a pull request if you have suggestions (as long as they don’t make the example too complex).

I recommend reading the files in this order:

  1. package.json – it lists dependencies
  2. app.js – it sets everything up
  3. the files in routes – these link the routes to controller functions
  4. the files in controllers – these contain the functions that generate the pages and send responses
  5. the files in views – these are the template files that get rendered by the controller functions

Other Resources

This video shows similar functionality coded from scratch. It’s in Portuguese, but the code is readable.

Awesome, thanks! I will try this tomorrow, winding down for today. :slight_smile:

1 Like

OK, thanks again for the tutorial! I am just now setting this up.

I managed to get myfooddata.local working, but I can’t get the subdomain tools.myfooddata.local working. Here is what I have so far:

const express = require("express"); // import express
const vhost = require("vhost"); //For subdomain
const hbs = require('hbs'); //For handlebars partials (like php includes)
const initHandlebars = require("./handlebars"); //Handlebars helper functions

//For nodemon
////--watch contollers/* -e js

require("dotenv").config(); //My SQL configuration
const mysql = require("mysql"); //Adding in MySQL
const db = require("./models/database"); //Points to SQL connections

// create an Express application
const app = express();
const tools = express(); //for subdomain

const domain =
    process.NODE_ENV === "production" ? "myfooddat.com" : "myfooddata.local";

app.use(vhost(`tools.${domain}`, tools));

//Point to view directory and declare use of handlebars
const path = require("path");
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "hbs");
app.use(express.static(path.join(__dirname, "public")));

//app.set('env development');

// This imports the routers = defines where the files are for routing
const indexRouter = require("./routes/index");
const toolsRouter = require("./routes/tools");
const apiRouter = require("./routes/api");

// Each router gets mounted here
app.use("/", indexRouter);
app.use("/tools", toolsRouter);
app.use("/api", apiRouter);

I think it must be something with the router file.
Which looks like this:

const express = require("express");
const ctrl = require("../controllers/toolsController");

const router = express.Router();

/* This runs the code stored in `controllers/pageController.js`
The point of this is so you can call different templates depending on the "Get Request"
or page request*/
//router.get("/", ctrl.homepage);
router.get("/nutrition-facts.php", ctrl.nutritionFacts);
router.get("/recipe-nutrition-calculator.php", ctrl.nutritionCalculator);
router.get("/nutrition-comparison.php", ctrl.nutritionComparison);
router.get("/protein-calculator.php", ctrl.proteinCalculator);

module.exports = router;

Calling it a day on my end, so no rush, or we can discuss this at the meetup tomorrow too. Thanks Josh! :slight_smile:

Did you add it to your /etc/hosts file? If not I could show you how to do that tomorrow.

It looks like a letter is missing there.

Hmm, yes, the subdomain is in the etc/hosts file. I can get it hvosts to work for http://myfooddata.local/ but not for the subdomain. I think it is probably something in the routing. I spent about 45 minutes on it and can’t get it so I will wait for the call.

Here is a resource I found that I was trying to implement:

I am having this issue where the subdomain index page loads the main site index page.

Going to try follow the Portuguese tutorial and report back.

1 Like

So I want to add to this guide about launching these subdomains on a server that uses Nginx.

If you do, it is helpful to have the subdomain listening on another port.

So in the example above you would have

// start the server
const PORT = 3000;
app.listen(PORT, () => console.log(`server is running at${PORT}`));

const dogPORT = 3001;
app.listen(dogPORT, () => console.log(`server is running at${dogPORT}`));

const catPORT = 3002;
app.listen(catPORT, () => console.log(`server is running at${catPORT}`));

Then in your Nginx server config you can add code for the new apps like so:

            location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;

    location /dog {
        proxy_pass http://localhost:3001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    location /cat {
        proxy_pass http://localhost:3002;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;

You will also need to make a new nginx available sites file for cats.mydomain.com and dogs.mydomain.com and have it listen to the requisite port.

Similar to this example:

        root /var/www/cats.mydomain.com/html;
        index index.html index.htm index.php index.nginx-debian.html;

        server_name cats.mydomain.com;

        location / {
        proxy_pass http://localhost:3002;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;


At this point, your subdomain should load appropriately. Just be aware than any relative links such as “./linkto/images” will probably not work in the subdomain as the main domain!