Advertisement
  1. Code
  2. JavaScript
  3. Node

Build a Complete MVC Website With Express

Scroll to top

What Is Express?

Express is one of the best frameworks for Node.js. It has great support and a bunch of helpful features. There are a lot of great articles out there, which cover all of the basics. However, this time I want to dig in a little bit deeper and share my workflow for creating a complete website. In general, this article is not only for Express, but for using it in combination with some other great tools that are available for Node developers.

To follow along with this tutorial, I'm assuming you're somewhat familiar with Node and have it installed on your system already.

Understanding Middleware in Express

At the heart of Express is Connect. This is a middleware framework, which comes with a lot of useful stuff. If you're wondering what exactly middleware is, here is a quick example:

1
const connect = require('connect'),
2
    http = require('http');
3
4
const app = connect()
5
    .use(function(req, res, next) {
6
        console.log("That's my first middleware");
7
        next();
8
    })
9
    .use(function(req, res, next) {
10
        console.log("That's my second middleware");
11
        next();
12
    })
13
    .use(function(req, res, next) {
14
        console.log("end");
15
        res.end("hello world");
16
    });
17
18
http.createServer(app).listen(3000);

Middleware is basically a function which accepts request and response objects and a next function. Each piece of middleware can decide to respond by using a response object or pass the flow to the next function by calling the next callback. In the example above, if you remove the next() method call in the second middleware, the hello world string will never be sent to the browser. In general, that's how Express works.

There's also some predefined middleware, which of course can save you a lot of time. For example, Body parser parses request bodies and supports application/json, application/x-www-form-urlencoded, and multipart/form-data. And Cookie parser parses cookie headers and populates req.cookies with an object keyed by the cookie's name.

Express actually wraps Connect and adds some new functionality around it, like routing logic, which makes the process much smoother. Here's an example of handling a GET request in Express:

1
app.get('/hello.txt', function(req, res){
2
    var body = 'Hello World';
3
    res.setHeader('Content-Type', 'text/plain');
4
    res.setHeader('Content-Length', body.length);
5
    res.end(body);
6
});

Source Code

The source code for this sample site that we built is available on GitHub. Feel free to fork it and play with it. Here are the steps for running the site.

  • download the source code
  • go to the app directory
  • run npm install
  • run the MongoDB daemon
  • run node app.js

1. Set Up Express

There are two ways to set up Express. The first one is by placing it in your package.json file and running npm install.

1
{
2
    "name": "MyWebSite",
3
    "description": "My website",
4
    "version": "0.0.1",
5
    "dependencies": {
6
        "express": "5.x"
7
    }
8
}

The framework's code will be placed in node_modules, and you will be able to create an instance of it. However, I prefer an alternative option, by using the command-line tool. By using npx express-generator, you now have a brand new CLI instrument. For example, if you run:

1
npx express-generator --sessions --css less --hbs app

Express will create an application skeleton with a few things already configured for you. Here are the usage options for the npx express-generator command:

1
  Usage: express [options] [dir]
2
3
  Options:
4
5
        --version        output the version number
6
    -e, --ejs            add ejs engine support
7
        --pug            add pug engine support
8
        --hbs            add handlebars engine support
9
    -H, --hogan          add hogan.js engine support
10
    -v, --view <engine>  add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)
11
        --no-view        use static html instead of view engine
12
    -c, --css <engine>   add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)
13
        --git            add .gitignore
14
    -f, --force          force on non-empty directory
15
    -h, --help           output usage information

As you can see, there are just a few options available, but for me they are enough. Normally, I use Less as the CSS preprocessor and handlebars as the templating engine. In this example, we will also need session support, so the --sessions argument solves that problem. When the above command finishes, our project looks like the following:

1
/public
2
    /images
3
    /javascripts
4
    /stylesheets
5
        /style.less
6
/routes
7
    /index.js
8
    /users.js
9
/views
10
    /index.hbs
11
    /error.hbs
12
    /layout.hbs
13
/app.js
14
/package.json

If you check out the package.json file, you will see that all the dependencies which we need are added here, although they haven't been installed yet. To do so, just run npm install, and a node_modules folder will pop up.

I realize that the above approach is not always appropriate. You may want to place your route handlers in another directory or something similar. But, as you'll see in the next few sections, I'll make changes to the structure that's already generated, and it's pretty easy to do. So you should just think of the npx express-generator command as a boilerplate generator.


2. Create the FastDelivery Site Design

For this tutorial, I designed a simple website of a fake company named FastDelivery. Here's a screenshot of the complete design:

FastDelivery website designFastDelivery website designFastDelivery website design

At the end of this tutorial, we will have a complete web application, with a working control panel. The idea is to manage every part of the site in separate restricted areas. The layout was created in Photoshop and sliced into CSS (less) and HTML (handlebars) files. After the slicing, we have the following files and app structure:

1
/public
2
    /images (there are several images exported from Photoshop)
3
    /javascripts
4
    /stylesheets
5
        /home.less
6
        /inner.less
7
        /style.css
8
        /style.less (imports home.less and inner.less)
9
/routes
10
    /index.js
11
/templates
12
    /index.hbs (home page)
13
    /layout.hbs (template for every other page of the site)
14
/app.js
15
/package.json

Here's a list of the site's elements that we are going to administrate:

  • Home: the banner in the middle with title and text
  • Blog: adding, removing, and editing of articles
  • Services page
  • Careers page
  • Contacts page

3. Scaffold the Express Project

There are a few things we need to do to get everything ready to start.

Step 1. File Structure

We need to do two things to get the files ready. First, we need to remove and add the proper files to match the file structure above. Second, we need to edit app.js to work with our new file structure. We need to remove these two lines:

1
const usersRouter = require("./routes/users");
2
...
3
app.use("/users", usersRouter);

Step 2. Configuration

Now, we need to set up the configuration. Let's imagine that our little site should be deployed to three different places: a local server, a staging server, and a production server. Of course, the settings for every environment are different, and we should implement a mechanism which is flexible enough. As you know, every node script is run as a console program. So we can easily send command-line arguments which will define the current environment. I wrapped that part in a separate module in order to write a test for it later. Here is the /config/index.js file:

1
const config = {
2
    local: {
3
        mode: 'local',
4
        port: 3000
5
    },
6
    staging: {
7
        mode: 'staging',
8
        port: 4000
9
    },
10
    production: {
11
        mode: 'production',
12
        port: 5000
13
    }
14
}
15
module.exports = function(mode) {
16
    return config[mode || process.argv[2] || 'local'] || config.local;
17
}

There are only two settings (for now): mode and port. As you may have guessed, the application uses different ports for the different servers. That's why we have to update the entry point of the site in app.js.

1
const config = require('./config')();
2
process.env.PORT = config.port;

To switch between the configurations, just add the environment at the end. For example:

1
npm start staging

Will run the server at port 4000.

Now we have all our settings in one place, and they are easily manageable.


Step 3. Create a Test Framework

I'm a big fan of Test-Driven Development (TDD). I'll try to cover all the base classes used in this article. Of course, having tests for absolutely everything would make this writing too long, but in general, that's how you should proceed when creating your own apps. One of my favorite frameworks for testing is uvu, because it is very easy to use and fast. Of course, it is available in the NPM registry:

1
npm install --save-dev uvu

Then, create a new script inside the "scripts" property in your package.json for testing.

1
"scripts": {
2
    "start": "node ./bin/www",
3
    "test": "uvu tests"
4
  },

Let's create a tests directory which will hold our tests. The first thing that we are going to check is our configuration setup. We will call the file for testing configuration config.js, and put it in tests.

1
const { test } = require("uvu");
2
const assert = require("uvu/assert");
3
4
test("Local configuration loading", function () {
5
  const config = require("../config")();
6
  assert.is(config.mode, "local");
7
});
8
test("Staging configuration loading", function () {
9
  const config = require("../config")("staging");
10
  assert.is(config.mode, "staging");
11
});
12
test("Production configuration loading", function () {
13
  const config = require("../config")("production");
14
  assert.is(config.mode, "production");
15
});
16
test.run();

Run npm test and you should see the following:

1
config.js
2
• • •   (3 / 3)
3
4
  Total:     3
5
  Passed:    3
6
  Skipped:   0
7
  Duration:  0.81ms

This time, I wrote the implementation first and the test second. That's not exactly the TDD way of doing things, but over the next few sections I'll do the opposite.

I strongly recommend spending a good amount of time writing tests. There is nothing better than a fully tested application.

A couple of years ago, I realized something very important, which may help you to produce better programs. Each time you start writing a new class, a new module, or just a new piece of logic, ask yourself:

How can I test this?

The answer to this question will help you to code much more efficiently, create better APIs, and put everything into nicely separated blocks. You can't write tests for spaghetti code. For example, in the configuration file above (/config/index.js) I added the possibility to send the mode in the module's constructor. You may wonder, why do I do that when the main idea is to get the mode from the command-line arguments? It's simple... because I needed to test it. Let's imagine that one month later I need to check something in a production configuration, but the node script is run with a staging parameter. I won't be able to make this change without that little improvement. That one previous little step now actually prevents problems in the future.


Step 4. Add a Database

Since we are building a dynamic website, we need a database to store our data in. I chose to use MongoDB for this tutorial. Mongo is a NoSQL document database. The installation instructions can be found in the documentation, and because I'm a Windows user, I followed the Windows installation. Once you finish with the installation, run the MongoDB daemon, which by default listens on port 27017. So, in theory, we should be able to connect to this port and communicate with the MongoDB server. To do this from a node script, we need a MongoDB module/driver. To add it, just run npm install mongodb.

Next, we are going to write a test, which checks if there is a mongodb server running. Here is the /tests/mongodb.js file:

1
const { test } = require("uvu");
2
const { MongoClient } = require("mongodb");
3
4
test("MongoDB server active", async function () {
5
  const client = new MongoClient("mongodb://127.0.0.1:27017/fastdelivery");
6
  await client.connect();
7
});
8
9
test.run();
10

We don't need to add any assert statement since the code will already throw an error if there are any problems. The callback in the .connect method of the MongoDB client receives a db object. We will use it later to manage our data, which means that we need access to it inside our models. It's not a good idea to create a new MongoClient object every time we have to make a request to the database. Because of that, we should connect to the database in the initial server creation. To do this, in /bin/www, we need to change a few lines:

1
// Replace  this

2
server.listen(port);
3
server.on("error", onError);
4
server.on("listening", onListening);
5
// With this

6
(async function () {
7
  const { MongoClient } = require("mongodb");
8
  const client = new MongoClient("mongodb://127.0.0.1:27017/fastdelivery");
9
  await client.connect();
10
  app.use(function (req, res, next) {
11
    req.db = client.db;
12
    next();
13
  });
14
  const server = http.createServer(app);
15
  server.listen(port);
16
  server.on("error", onError);
17
  server.on("listening", onListening);
18
})();

Even better, since we have a configuration setup, it would be a good idea to place the MongoDB host and port in there and then change the connect URL to:

1
`mongodb://${config.mongo.host}:${config.mongo.client}/fastdelivery`

Now, all Express handlers will have the req.db property available, due to the middleware automatically running before each request.


4. Set Up an MVC Pattern With Express

We all know the MVC pattern. The question is how this applies to Express. More or less, it's a matter of interpretation. In the next few steps I'll create modules, which act as a model, view, and controller.

Step 1. Model

The model is what will be handling the data that's in our application. It should have access to a db object, returned by MongoClient. Our model should also have a method for extending it, because we may want to create different types of models. For example, we might want a BlogModel or a ContactsModel. So we need to write a new spec, /tests/base.model.js, in order to test these two model features. And remember, by defining these functionalities before we start coding the implementation, we can guarantee that our module will do only what we want it to do.

1
const { test } = require("uvu");
2
const assert = require("uvu/assert");
3
const ModelClass = require("../models/base");
4
const dbMockup = {};
5
test("Module creation", async function () {
6
  const model = new ModelClass(dbMockup);
7
  assert.ok(model.db);
8
  assert.ok(model.setDB);
9
  assert.ok(model.collection);
10
});
11
test.run();

Instead of a real db object, I decided to pass a mockup object. That's because later, I may want to test something specific, which depends on information coming from the database. It will be much easier to define this data manually.

1
module.exports = class BaseModel {
2
  constructor(db) {
3
    this.setDB(db);
4
  }
5
  setDB(db) {
6
    this.db = db;
7
  }
8
  collection() {
9
    if (this._collection) return this._collection;
10
    return (this._collection = this.db.collection("fastdelivery-content"));
11
  }
12
};

Here, there are two helper methods: a setter for the db object and a getter for our database collection.

Step 2. View

The view will render information to the screen. Essentially, the view is a class which sends a response to the browser. Express provides a short way to do this:

1
res.render('index', { title: 'Express' });

The response object is a wrapper, which has a nice API, making our life easier. However, I'd prefer to create a module which will encapsulate this functionality. The default views directory will be changed to templates, and a new one will be created, which will host the Base view class. This little change now requires another change. We should notify Express that our template files are now placed in another directory:

1
app.set("views", path.join(__dirname, "templates"));

First, I'll define what I need, write the test, and then write the implementation. We need a module matching the following rules:

  • Its constructor should receive a response object and a template name.
  • It should have a render method which accepts a data object.
  • It should be extendable.

You may wonder why I'm extending the View class. Isn't it just calling the response.render method? Well, in practice, there are cases in which you will want to send a different header or maybe manipulate the response object somehow—for example, serving JSON data:

1
const data = { developer: "Krasimir Tsonev" };
2
response.contentType("application/json");
3
response.send(JSON.stringify(data));

Instead of doing this every time, it would be nice to have an HTMLView class and a JSONView class, or even an XMLView class for sending XML data to the browser. It's just better, if you build a large website, to wrap such functionalities instead of copy-pasting the same code over and over again.

Here is the spec for the /views/Base.js:

1
const { test } = require("uvu");
2
const assert = require("uvu/assert");
3
4
const ViewClass = require("../views/base");
5
6
test("View creation and rendering", function () {
7
  let responseMockup = {
8
    render: function (template, data) {
9
      assert.is(data.myProperty, "value");
10
      assert.is(template, "template-file");
11
    },
12
  };
13
  let view = new ViewClass(responseMockup, "template-file");
14
  view.render({ myProperty: "value" });
15
});
16
17
test.run();

In order to test the rendering, I had to create a mockup. In this case, I created an object which imitates Express's response object. Here is the /views/base.js class.

1
module.exports = class BaseView {
2
  constructor(response, template) {
3
    this.response = response;
4
    this.template = template;
5
  }
6
  render(data) {
7
    if (this.response && this.template) {
8
      this.response.render(this.template, data);
9
    }
10
  }
11
};

Now we have three specs in our tests directory, and if you run npm test, everything should pass.

Step 3. Controller

Remember the routes and how they were defined?

1
app.get('/', routes.index);

The '/' after the route—which, in the example above, is actually the controller—is just a middleware function which accepts request, response, and next.

1
exports.index = function(req, res, next) {
2
    res.render('index', { title: 'Express' });
3
};

Above is how your controller should look in the context of Express. The express(1) command-line tool creates a directory named routes, which is for this.

Since we're not just building a tiny application, it would be wise if we created a base class, which we can extend. If we ever need to pass some kind of functionality to all of our controllers, this base class would be the perfect place. Again, I'll write the test first, so let's define what we need:

  • a child instance should have a run method, which is the old middleware function
  • there should be a name property, which identifies the controller
  • we should be able to create independent objects, based on the class

So just a few things for now, but we may add more functionality later. The test would look something like this:

1
const { test } = require("uvu");
2
const assert = require("uvu/assert");
3
4
const ControllerClass = require("../routes/base");
5
6
test("Children creation", function () {
7
  class ChildClass extends ControllerClass {
8
    constructor() {
9
      super("my child controller");
10
    }
11
  }
12
  const child = new ChildClass();
13
  assert.ok(child.run);
14
  assert.equal(child.name, "my child controller");
15
});
16
17
test("Children differentiation", function () {
18
  class ChildAClass extends ControllerClass {
19
    constructor() {
20
      super("child A");
21
    }
22
    customProperty = "value";
23
  }
24
  class ChildBClass extends ControllerClass {
25
    constructor() {
26
      super("child B");
27
    }
28
  }
29
  const childA = new ChildAClass();
30
  const childB = new ChildBClass();
31
  assert.is.not(childA.name, childB.name);
32
  assert.not(childB.customProperty);
33
});
34
35
test.run();

And here is the implementation of /routes/base.js:

1
module.exports = class BaseController {
2
  constructor(name) {
3
    this.name = name;
4
  }
5
  run(req, res, next) {}
6
};

Of course, every child class should define its own run method, along with its own logic.


5. Create the FastDelivery Website

OK, we have a good set of classes for our MVC architecture, and we've covered our newly created modules with tests. Now we are ready to continue with the site of our fake company, FastDelivery.

Let's imagine that the site has two parts: a front-end and an administration panel. The front-end will be used to display the information written in the database to our end users. The admin panel will be used to manage that data. Let's start with our admin (control) panel.

Step 1. Control Panel

Let's first create a simple controller which will serve as the administration page. Here's the /routes/admin.js file:

1
const BaseController = require("./base"),
2
  View = require("../views/base");
3
module.exports = new (class AdminController extends BaseController {
4
  constructor() {
5
    super("admin");
6
  }
7
  run(req, res, next) {
8
    if (this.authorize(req)) {
9
      req.session.fastdelivery = true;
10
      req.session.save(function (err) {
11
        var v = new View(res, "admin");
12
        v.render({
13
          title: "Administration",
14
          content: "Welcome to the control panel",
15
        });
16
      });
17
    } else {
18
      const v = new View(res, "admin-login");
19
      v.render({
20
        title: "Please login",
21
      });
22
    }
23
  }
24
  authorize(req) {
25
    return (
26
      (req.session &&
27
        req.session.fastdelivery &&
28
        req.session.fastdelivery === true) ||
29
      (req.body &&
30
        req.body.username === this.username &&
31
        req.body.password === this.password)
32
    );
33
  }
34
})();

By using the pre-written base classes for our controllers and views, we can easily create the entry point for the control panel. The View class accepts the name of a template file. According to the code above, the file should be called admin.js and should be placed in /templates. The content would look something like this:

1
<!DOCTYPE html>
2
<html>
3
    <head>
4
        <title>{{ title }}</title>
5
        <link rel='stylesheet' href='/stylesheets/style.css' />
6
    </head>
7
    <body>
8
        <div class="container">
9
            <h1>{{ content }}</h1>
10
        </div>
11
    </body>
12
</html>

In order to keep this tutorial fairly short and in an easy-to-read format, I'm not going to show every single view template. I strongly recommend that you download the source code from GitHub.

Now, to make the controller visible, we have to add a route to it in app.js:

1
const admin = require('./routes/admin');
2
...
3
app.all("/admin*", function (req,res,next) {
4
  admin.run(req,res,next)
5
})

Note that we are not sending the Admin.run method directly as middleware. That's because we want to keep the context. If we do this:

1
app.all('/admin*', admin.run);

the word this in Admin will point to something else.

Protecting the Administration Panel

Every page which starts with /admin should be protected. To achieve this, we are going to use Express's middleware: Session. It simply attaches an object to the request called session. We should now change our Admin controller to do two additional things:

  • It should check if there is a session available. If not, then display a login form.
  • It should accept the data sent by the login form and authorize the user if the username and password match.

Here is a little helper function we can use to accomplish this:

1
authorize(req) {
2
    return (
3
      (req.session &&
4
        req.session.fastdelivery &&
5
        req.session.fastdelivery === true) ||
6
      (req.body &&
7
        req.body.username === this.username &&
8
        req.body.password === this.password)
9
    );
10
  }

First, we have a statement which tries to recognize the user via the session object. Secondly, we check if a form has been submitted. If so, the data from the form is available in the request.body object, which is filled by the bodyParser middleware. Then we just check if the username and password match.

And now here is the run method of the controller, which uses our new helper. We check if the user is authorized, displaying the control panel if so, and otherwise we display the login page:

1
run(req, res, next) {
2
    if (this.authorize(req)) {
3
      req.session.fastdelivery = true;
4
      req.session.save(function (err) {
5
        var v = new View(res, "admin");
6
        v.render({
7
          title: "Administration",
8
          content: "Welcome to the control panel",
9
        });
10
      });
11
    } else {
12
      const v = new View(res, "admin-login");
13
      v.render({
14
        title: "Please login",
15
    });
16
}

Step 2. Managing Content

As I pointed out at the beginning of this article, we have plenty of things to administrate. To simplify the process, let's keep all the data in one collection. Every record will have a title, text, picture, and type property. The type property will determine the owner of the record. For example, the Contacts page will need only one record with type: 'contacts', while the Blog page will require more records. So we need three new pages for adding, editing, and showing records. Before we jump into creating new templates, styling, and putting new stuff into the controller, we should write our model class, which stands between the MongoDB server and our application and of course provides a meaningful API.

1
// /models/content.js

2
3
const Base = require("./base"),
4
  crypto = require("node:crypto");
5
class ContentModel extends Base {
6
  constructor(db) {
7
    super(db);
8
  }
9
  insert(data, callback) {
10
    data.ID = crypto.randomBytes(20).toString("hex");
11
    this.collection().insert(data, {}, callback || function () {});
12
  }
13
  update(data, callback) {
14
    this.collection().update(
15
      { ID: data.ID },
16
      data,
17
      {},
18
      callback || function () {}
19
    );
20
  }
21
  getlist(callback, query) {
22
    this.collection()
23
      .find(query || {})
24
      .toArray(callback);
25
  }
26
  remove(ID, callback) {
27
    this.collection().findAndModify(
28
      { ID: ID },
29
      [],
30
      {},
31
      { remove: true },
32
      callback
33
    );
34
  }
35
}
36
module.exports = ContentModel;

The model takes care of generating a unique ID for every record. We will need it in order to update the information later on.

If we want to add a new record for our Contacts page, we can simply use:

1
const model = new (require("../models/ContentModel"));
2
model.insert({
3
    title: "Contacts",
4
    text: "...",
5
    type: "contacts"
6
});

So we have a nice API to manage the data in our MongoDB collection. Now we are ready to write the UI for using this functionality. For this part, the admin controller will need to be changed quite a bit. To simplify the task, I decided to combine the list of the added records and the form for adding/editing them. As you can see in the screenshot below, the left part of the page is reserved for the list and the right part for the form.

control-panelcontrol-panelcontrol-panel

Having everything on one page means that we have to focus on the part which renders the page or, to be more specific, on the data which we are sending to the template. That's why I created several helper functions which are combined, like so:

1
this.del(req, function () {
2
    this.form(req, res, function (formMarkup) {
3
        this.list(function (listMarkup) {
4
              v.render({
5
                title: "Administration",
6
                content: "Welcome to the control panel",
7
                list: listMarkup,
8
                form: formMarkup,
9
            });
10
        });
11
    });
12
});
13
const v = new View(res, "admin");

It looks a bit ugly, but it works as I wanted. The first helper is a del method which checks the current GET parameters, and if it finds action=delete&id=[id of the record], it removes data from the collection. The second function is called form, and it is responsible mainly for showing the form on the right side of the page. It checks if the form is submitted and properly updates or creates records in the database. At the end, the list method fetches the information and prepares an HTML table, which is later sent to the template. The implementation of these three helpers can be found in the source code for this tutorial.

Here, I've decided to show you the function which handles the file upload in admin.js:

1
handleFileUpload(req) {
2
    if (!req.files || !req.files.picture || !req.files.picture.name) {
3
      return req.body.currentPicture || "";
4
    }
5
    const data = fs.readFileSync(req.files.picture.path);
6
    const fileName = req.files.picture.name;
7
    const uid = crypto.randomBytes(10).toString("hex");
8
    const dir = __dirname + "/../public/uploads/" + uid;
9
    fs.mkdirSync(dir, "0777");
10
    fs.writeFileSync(dir + "/" + fileName, data);
11
    return "/uploads/" + uid + "/" + fileName;
12
  }

If a file is submitted, the node script .files property of the request object is filled with data. In our case, we have the following HTML element:

1
<input type="file" name="picture" />

This means that we could access the submitted data via req.files.picture. In the code snippet above, req.files.picture.path is used to get the raw content of the file. Later, the same data is written in a newly created directory, and at the end, a proper URL is returned. All of these operations are synchronous, but it's a good practice to use the asynchronous version of readFileSync, mkdirSync, and writeFileSync.

Step 3. The Front-End

The hard work is now complete. The administration panel is working, and we have a ContentModel class, which gives us access to the information stored in the database. What we have to do now is to write the front-end controllers and bind them to the saved content.

Here is the controller for the Home page: /controllers/index.js

1
const BaseController = require("./")
2
const model = new (require("../models/content"))
3
module.exports = class HomeController extends BaseController{
4
  constructor() {
5
    super("Home")
6
    this.content = null;
7
  }
8
  run(req, res, next) {
9
    model.setDB(req.db);
10
    const self = this;
11
    this.getContent(function () {
12
      const v = new View(res, "home");
13
      v.render(self.content);
14
    });
15
  }
16
  getContent(callback) {
17
    const self = this;
18
    this.content = {};
19
    model.getlist(
20
      function (err, records) {
21
        // ... storing data to content object

22
        model.getlist(
23
          function (err, records) {
24
            // ... storing data to content object

25
            callback();
26
          },
27
          { type: "blog" }
28
        );
29
      },
30
      { type: "home" }
31
    );
32
  }
33
};

The home page needs one record with a type of home and four records with a type of blog. Once the controller is done, we just have to add a route to it in app.js:

1
app.all("/", function (req, res, next) {
2
  home.run(req, res, next);
3
});

Again, we are attaching the db object to the request. It's pretty much the same workflow as the one used in the administration panel.

The other pages for our front-end (client side) are almost identical, in that they all have a controller, which fetches data by using the model class and of course a route defined. There are two interesting situations which I'd like to explain in more detail. The first one is related to the blog page. It should be able to show all the articles, but also to present only one. So we have to register two routes:

1
app.all("/blog/:id", function (req, res, next) {
2
  Blog.runArticle(req, res, next);
3
});
4
app.all("/blog", function (req, res, next) {
5
  Blog.run(req, res, next);
6
});

They both use the same controller, Blog, but call different run methods. Pay attention to the /blog/:id string. This route will match URLs like /blog/4e3455635b4a6f6dccfaa1e50ee71f1cde75222b, and the long hash will be available in req.params.id. In other words, we are able to define dynamic parameters. In our case, that's the ID of the record. Once we have this information, we can create a unique page for every article.

The second interesting part is how I built the Services, Careers, and Contacts pages. It is clear that they use only one record from the database. If we had to create a different controller for every page, then we'd have to copy/paste the same code and just change the type field. There is a better way to achieve this, though, by having only one controller, which accepts the type in its run method. So here are the routes:

1
app.all("/services", function (req, res, next) {
2
  Page.run("services", req, res, next);
3
});
4
app.all("/careers", function (req, res, next) {
5
  Page.run("careers", req, res, next);
6
});
7
app.all("/contacts", function (req, res, next) {
8
  Page.run("contacts", req, res, next);
9
});

And the controller would look like this:

1
module.exports = BaseController.extend({ 
2
    name: "Page",
3
    content: null,
4
    run: function(type, req, res, next) {
5
        model.setDB(req.db);
6
        var self = this;
7
        this.getContent(type, function() {
8
            var v = new View(res, 'inner');
9
            v.render(self.content);
10
        });
11
    },
12
    getContent: function(type, callback) {
13
        var self = this;
14
        this.content = {}
15
        model.getlist(function(err, records) {
16
            if(records.length > 0) {
17
                self.content = records[0];
18
            }
19
            callback();
20
        }, { type: type });
21
    }
22
});

6. Deployment

Deploying an Express-based website is actually the same as deploying any other Node.js application:

  1. The files are placed on the server.
  2. The node process should be stopped (if it is running).
  3. An npm install command should be run in order to install the new dependencies (if any).
  4. The main script should then be run again.

Keep in mind that Node is still fairly young, so not everything may work as you expected, but there are improvements being made all the time. For example, forever guarantees that your Node.js program will run continuously. You can do this by issuing the following command:

1
forever start yourapp.js

This is what I'm using on my servers as well. It's a nice little tool, but it solves a big problem. If you run your app with just node yourapp.js, once your script exits unexpectedly, the server goes down. forever simply restarts the application.

Now I'm not a system administrator, but I wanted to share my experience integrating node apps with Apache or Nginx because I think that this is somehow part of the development workflow.

As you know, Apache normally runs on port 80, which means that if you open http://localhost or http://localhost:80, you will see a page served by your Apache server, and most likely your node script is listening on a different port. So you need to add a virtual host that accepts the requests and sends them to the right port. For example, let's say that I want to host the site that we've just built on my local Apache server under the expresscompletewebsite.dev address. The first thing that we have to do is to add our domain to the hosts file.

1
127.0.0.1   expresscompletewebsite.dev

After that, we have to edit the httpd-vhosts.conf file under the Apache configuration directory and add:

1
# expresscompletewebsite.dev
2
<VirtualHost *:80>
3
    ServerName expresscompletewebsite.dev
4
    ServerAlias www.expresscompletewebsite.dev
5
    ProxyRequests off
6
    <Proxy *>
7
        Order deny,allow
8
        Allow from all
9
    </Proxy>
10
    <Location />
11
        ProxyPass http://localhost:3000/
12
        ProxyPassReverse http://localhost:3000/
13
    </Location>
14
</VirtualHost>

The server still accepts requests on port 80 but forwards them to port 3000, where node is listening.

The Nginx setup is much easier and, to be honest, it's a better choice for hosting Node.js-based apps. You still have to add the domain name in your hosts file. After that, simply create a new file in the /sites-enabled directory under the Nginx installation. The content of the file would look something like this:

1
server {
2
    listen 80;
3
    server_name expresscompletewebsite.dev
4
    location / {
5
            proxy_pass http://127.0.0.1:3000;
6
            proxy_set_header Host $http_host;
7
    }
8
}

Keep in mind that you can't run both Apache and Nginx with the above hosts setup. That's because they both require port 80. Also, you may want to do a bit of additional research about better server configuration if you plan to use the above code snippets in a production environment. As I said, I'm not an expert in this area.


Conclusion

Express is a great framework, which gives you a good starting point to begin building your applications. As you can see, it's a matter of choice on how you will extend it and what you will use to build with it. It simplifies the boring tasks by using some great middleware and leaves the fun parts to the developer.

This post has been updated with contributions from Jacob Jackson. Jacob is a web developer, technical writer, freelancer, and open-source contributor.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.