DEV Community

Cover image for ExpressJS Project Structure
Brian Emilius
Brian Emilius

Posted on • Originally published at brianemilius.com

ExpressJS Project Structure

This article was originally published on www.brianemilius.com/expressjs-structure. Visit www.brianemilius.com/blog for more content.


ExpressJS is probably the most popular framework for creating server-side web applications in Node.js.
In this blog series I want to show you how to get started and how to do it The Right Way™.

Prerequisites

  • Basic knowledge of Node.js and npm
  • Some intermediate JavaScript knowledge
  • Basic HTML and CSS

Intro

In the previous blog post I introduced you to ExpressJS with a very simple example code.

For this post I want to show you how to build a proper project structure for your ExpressJS application, that will scale and is robust.

The keyword for what I want to do here is "fragmentation". We are going to divide the application into a lot of small, modular files.

The reasoning behind this choice is

  • smaller files are easier to maintain
  • collaboration runs more smoothly on version control platforms such as GitHub
  • you can inject or remove functionality without breaking the application in other places

The folder structure

Let's start out by taking a look at the folder structure

my-express-app/
|   app.js
|   package.json
|
└───node_modules/
|   |   ...
|
└───bin/
|   |   www
|
└───config/
|   |   parsing.js
|   |   view-engine.js
|   |   static-files.js
|   |   database.js
|
└───controllers/
|   |   page.controller.js
|
└───models/
|   |   page.model.js
|
└───views/
|   |   page.ejs
|   |
|   └───partials/
|       |   head.ejs
|       |   seo.ejs
|       |   header.ejs
|       |   footer.ejs
|       |   after-footer.ejs
|
└───routes/
|   |   page.route.js
|   |   router.js
|
assets/
    |   ...
Enter fullscreen mode Exit fullscreen mode

Model, View, Controller

The folders models, views, and controllers are at the very core of the application functionality and is meant to use the programming design pattern "MVC".

Configuration

In the folder config we will keep all the little configuration files that express makes use of. Anything from database setup, which parser to use, or what view engine the application should use.

The router

I've taken routing logic completely out of the main application file and placed it by itself. As I mention in the previous blog post, I like to keep application configuration and logic separate from the router engine. Basically, the router engine should be able to function on its own. This is a great pattern for testing purposes, in my opinion.

www and app.js

The app.js file is the main application file. Here we gather all the threads from the configuration and router. This file is also meant to handle application errors and provide a flow for error pages (for example a 404 page) and error reporting (for example logging).

The www file is the application executable. This file starts the server and handles any server-related errors. We can have several different executables for different situations. An example could be one executable file for development and one for the production environment.

Static files

Finally, most websites have a bunch of static files that needs to be served so they can be viewed in the browser. These are files such as stylesheets, front-end related JavaScript, images and other media files. These files are kept in the assets folder.

Tie the fragments together

First, let's create the main application file app.js with the following code:

// app.js

let express = require("express");
let app = express();

// Here, we will require the configuration files

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

That's it, really. For now. We still need to add requirements for some of our configuration files and the router, but all that comes later. This is the very least we need for the application to run.

The next file we want to create is the bin/www executable. There are 3 things we need to do in this file:

  • require the application
  • create a server instance and make it listen
  • handle errors related to the server instance
// bin/www

#!/usr/bin/env node
let app = require("../app");
let http = require("http");

// Define a port for the server to listen on
let port = process.env.PORT || 3000;
app.set("port", port);

// Create a server instance
let server = http.createServer(app);

// Make the server listen on a port
server.listen(port);

// Handle errors and success
server.on("error", onError);
server.on("listening", onListening);

function pipeOrPort(address) {
    return typeof address == "string" ? `pipe ${address}` : `port ${address.port}`;
}

function onError(error) {
    if (error.syscall != "listen") {
        throw error;
    }

    let bind = pipeOrPort(server.address());

    switch (error.code) {
        case "EACCES":
            console.error(`${bind} requires elevated privileges.`);
            process.exit(1);
            break;
        case "EADDRINUSE":
            console.error(`${bind} is already in use.`);
            process.exit(1);
            break;
        default:
            throw error;
    }
}

function onListening() {
    let bind = pipeOrPort(server.address());
    console.log(`Listening on ${bind}`);
}
Enter fullscreen mode Exit fullscreen mode

We start out by requiring the app module from app.js as well as the http package, which is a part of the Node.js general API.

let app = require("../app");
let http = require("http");
Enter fullscreen mode Exit fullscreen mode

Next, we define a port for the server to listen on. In this case, the port is set to whatever is in our environment variable PORT or, if the environment variable doesn't exist; 3000.

let port = process.env.PORT || 3000;
app.set("port", port);
Enter fullscreen mode Exit fullscreen mode

Then we create a server instance and make it listen on the port.

let server = http.createServer(app);
server.listen(port);
Enter fullscreen mode Exit fullscreen mode

The rest of the file is basic event handling. We set up listeners for 2 events: "error" and "listening".

server.on("error", onError);
server.on("listening", onListening);
Enter fullscreen mode Exit fullscreen mode

If the error event is triggered, we call the function onError which handles a few different scenarios, such as

  • the server cannot run because the port we defined is in use by another program
  • the server cannot run because the operating system requires certain privileges for a user to run a program

If the listening event is triggered, we log a short message to the console to let us know the server is running.

Configuration injection

Now that we've got the application and server set up and it's basically ready to run, I want to show you how you can tie in configuration modules. We will be making a single configuration module:

  • config for the location of our static files folder

We need two files.

// config/static-files.js

module.exports = function(express, app) {
    app.use("/assets", express.static("assets"));
};
Enter fullscreen mode Exit fullscreen mode

This configuration is a middleware function. It tells express to use a function, express.static("assets"), on requests to the "http://localhost:3000/assets" address and serve the folder "assets" as static content to the client.

Basically, this means that when a client navigates to "http://localhost:3000/assets" they have access to anything inside the "assets" folder.

I will dive deeper into what middlewares are in a later article.

Finally, inject this module to our app.js file.

// app.js

let express = require("express");
let app = express();

// Here, we will require the configuration files
require("./config/static-files")(express, app);

module.exports = app;
Enter fullscreen mode Exit fullscreen mode

Test all the things!

Now it's time to see if all we did actually work. To test the server, I want to have a HTML file in the assets folder, that prints out a simple message, so I can see that everything runs correctly so far.

<!-- assets/test.html -->

<h1>Hello there!</h1>
<p>If you can see this, the server is running and has been configured correctly.</p>
Enter fullscreen mode Exit fullscreen mode

Now save the file and run the server from your console with the command

node bin/www
Enter fullscreen mode Exit fullscreen mode

You should see the message

Listening on port 3000
Enter fullscreen mode Exit fullscreen mode

Then open your browser and navigate to http://localhost:3000/assets/test.html.

You should see this:

Browser window with a demo message

If you have followed along so far, I would really love your input. Are the concepts I demonstrate easy to understand or is there something you wish I explain more clearly?

I have prepared a small repository on GitHub, that you can clone and play around with. It contains everything covered in this post.

The next article will be about the router.

Articles in this series so far

Upcoming articles:

  • The ExpressJS Router
  • ExpressJS Template Engines
  • ExpressJS Middleware
  • How To Parse Content From Forms With ExpressJS

Construction photo created by jcomp - www.freepik.com

Latest comments (9)

Collapse
 
misterhtmlcss profile image
Roger K.

Looking forward to the next article.

Collapse
 
brianemilius profile image
Brian Emilius
Collapse
 
brianemilius profile image
Brian Emilius

Thank you!
It's in draft, but work has me busy. I won't forget to post it though. I promise 😊

Collapse
 
hailomym profile image
HailoMYM

Something that I found useful is to have a deeper layer inside constrollers that I usually call services.

What controllers do is to receive the request from the client, "manage" the data and pass it to the required services. The idea of services is to "do" the task without depending on the package or framework that is used.

So a controller receives the req object, extract the required info like the body or params and pass plain javascript objects to the services that does not care if we are using express or other tool.
You can find more about this structure in the following articule coreycleary.me/project-structure-f...

Collapse
 
brianemilius profile image
Brian Emilius

Hi @hailomym 👋

I 100% agree with the deeper layer. Sometimes I call it services, sometimes I have it split down even further into ORM and other services / business logic.

When I get to the MVC part of this project, I am going to dive into the reasons for this choice, so hang tight 😄

Collapse
 
orshee profile image
orshee

Why bin/www?
its out of scope of MVC
Doesnt work on windows, potentially other os-es too

For a boilerplate, two minuses

Collapse
 
brianemilius profile image
Brian Emilius

Hi @orshee 👋

Why bin/www? Because you might want other ways to run your application through. The www is "just" the server layer of the application. You could exchange it for a development-specific server layer and a production-specific layer. You could have one setup use (A) set of certificates and env settings, and the other use (B) set of certificates and so on. Or you might want to run it through a process manager such as PM2 or similar. The basic idea is that this split from the rest of the application helps with scalability. Which, incidentally is in the scope of this article.

Is it out of scope with MVC? Yes. Absolutely. I should hope it is since MVC takes care of the user interface and nothing else (bar some pseudo data negotiation).

Does this work on windows? Yep. It really does. It works on Mac, Windows, Debian, CentOS, Arch, and Mint, which are the platforms I test this on. No special setup on either of those machines. It works out of the box.

Why do you think it wouldn't?

This is not a boilerplate. It's an article about the express framework and how you can set it up in a scalable production-ready manner, with the theory behind the choices explained.

I've got to give your comment two minuses 😉

Collapse
 
isoloi profile image
ISOLOI

Great article. Wondering why you chose to use http module? Maybe explaing that express essentially is a wrapper, or incorporating https module might be a little more definitive. I'm sure the later parts will explain more! I'm always browsing for new boilerplate to handle demos so I appreciate the post!

Collapse
 
brianemilius profile image
Brian Emilius • Edited

Hi @isoloi 👋

That's a very good point you make. Yes, I chose the http module over the express native server functionality because of https or even http/2. The application I demonstrate is meant to handle scalability well, and in my opinion, this achieves it so far as the server is concerned.

I am planning a later series on deployment and the devOps aspects of running an Express application and this would be a great topic for one of the articles 😄