Home >Web Front-end >JS Tutorial >Build a Complete MVC Website With Express

Build a Complete MVC Website With Express

Jennifer Aniston
Jennifer AnistonOriginal
2025-03-14 09:33:11406browse

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:

const connect = require('connect'),<br>    http = require('http');<br><br>const app = connect()<br>    .use(function(req, res, next) {<br>        console.log("That's my first middleware");<br>        next();<br>    })<br>    .use(function(req, res, next) {<br>        console.log("That's my second middleware");<br>        next();<br>    })<br>    .use(function(req, res, next) {<br>        console.log("end");<br>        res.end("hello world");<br>    });<br><br>http.createServer(app).listen(3000);<br>

Middleware is basically a function which accepts response objects and a response object or pass the flow to the next function by calling the next() method call in the second middleware, the Body parser parses request bodies and supports application/json, application/x-www-form-urlencoded, and multipart/form-data. And 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:

app.get('/hello.txt', function(req, res){<br>    var body = 'Hello World';<br>    res.setHeader('Content-Type', 'text/plain');<br>    res.setHeader('Content-Length', body.length);<br>    res.end(body);<br>});<br>

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 npm install
  • run the MongoDB daemon
  • run npm install.
    {<br>    "name": "MyWebSite",<br>    "description": "My website",<br>    "version": "0.0.1",<br>    "dependencies": {<br>        "express": "5.x"<br>    }<br>}<br>

    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 command:

      Usage: express [options] [dir]<br><br>  Options:<br><br>        --version        output the version number<br>    -e, --ejs            add ejs engine support<br>        --pug            add pug engine support<br>        --hbs            add handlebars engine support<br>    -H, --hogan          add hogan.js engine support<br>    -v, --view <engine>  add view <engine> support (dust|ejs|hbs|hjs|jade|pug|twig|vash) (defaults to jade)<br>        --no-view        use static html instead of view engine<br>    -c, --css <engine>   add stylesheet <engine> support (less|stylus|compass|sass) (defaults to plain css)<br>        --git            add .gitignore<br>    -f, --force          force on non-empty directory<br>    -h, --help           output usage information<br></engine></engine></engine></engine>

    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 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 app.js to work with our new file structure. We need to remove these two lines:

    const usersRouter = require("./routes/users");<br>...<br>app.use("/users", usersRouter);<br>

    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:

    const config = {<br>    local: {<br>        mode: 'local',<br>        port: 3000<br>    },<br>    staging: {<br>        mode: 'staging',<br>        port: 4000<br>    },<br>    production: {<br>        mode: 'production',<br>        port: 5000<br>    }<br>}<br>module.exports = function(mode) {<br>    return config[mode || process.argv[2] || 'local'] || config.local;<br>}<br>

    There are only two settings (for now): 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.

    const config = require('./config')();<br>process.env.PORT = config.port;<br>

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

    npm start staging<br>

    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:

    npm install --save-dev uvu<br>

    Then, create a new script inside the npm test and you should see the following:

    config.js<br>• • •   (3 / 3)<br><br>  Total:     3<br>  Passed:    3<br>  Skipped:   0<br>  Duration:  0.81ms<br>

    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 production configuration, but the node script is run with a 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:

    const { test } = require("uvu");<br>const { MongoClient } = require("mongodb");<br><br>test("MongoDB server active", async function () {<br>  const client = new MongoClient("mongodb://127.0.0.1:27017/fastdelivery");<br>  await client.connect();<br>});<br><br>test.run();<br><br>

    We don't need to add any .connect method of the MongoDB client receives a 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 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 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 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.

    const { test } = require("uvu");<br>const assert = require("uvu/assert");<br>const ModelClass = require("../models/base");<br>const dbMockup = {};<br>test("Module creation", async function () {<br>  const model = new ModelClass(dbMockup);<br>  assert.ok(model.db);<br>  assert.ok(model.setDB);<br>  assert.ok(model.collection);<br>});<br>test.run();<br>

    Instead of a real db object and a getter for our database views directory will be changed to Base view class. This little change now requires another change. We should notify Express that our template files are now placed in another directory:

    app.set("views", path.join(__dirname, "templates"));<br>

    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 View class. Isn't it just calling the response object somehow—for example, serving JSON data:
      const data = { developer: "Krasimir Tsonev" };<br>response.contentType("application/json");<br>response.send(JSON.stringify(data));<br>

      Instead of doing this every time, it would be nice to have an JSONView class, or even an tests directory, and if you run '/' after the route—which, in the example above, is actually the controller—is just a middleware function which accepts response, and express(1) command-line tool creates a directory named run method, which is the old middleware function

    • there should be a 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:

      const BaseController = require("./base"),<br>  View = require("../views/base");<br>module.exports = new (class AdminController extends BaseController {<br>  constructor() {<br>    super("admin");<br>  }<br>  run(req, res, next) {<br>    if (this.authorize(req)) {<br>      req.session.fastdelivery = true;<br>      req.session.save(function (err) {<br>        var v = new View(res, "admin");<br>        v.render({<br>          title: "Administration",<br>          content: "Welcome to the control panel",<br>        });<br>      });<br>    } else {<br>      const v = new View(res, "admin-login");<br>      v.render({<br>        title: "Please login",<br>      });<br>    }<br>  }<br>  authorize(req) {<br>    return (<br>      (req.session &&<br>        req.session.fastdelivery &&<br>        req.session.fastdelivery === true) ||<br>      (req.body &&<br>        req.body.username === this.username &&<br>        req.body.password === this.password)<br>    );<br>  }<br>})();<br>

      By using the pre-written base classes for our controllers and views, we can easily create the entry point for the control panel. The Admin.run method directly as middleware. That's because we want to keep the context. If we do this:

      app.all('/admin*', admin.run);<br>

      the word 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 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:

      authorize(req) {<br>    return (<br>      (req.session &&<br>        req.session.fastdelivery &&<br>        req.session.fastdelivery === true) ||<br>      (req.body &&<br>        req.body.username === this.username &&<br>        req.body.password === this.password)<br>    );<br>  }<br>

      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 bodyParser middleware. Then we just check if the username and password match.

      And now here is the title, picture, and type property will determine the owner of the record. For example, the Contacts page will need only one record with 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.

      Build a Complete MVC Website With Express

      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:

      this.del(req, function () {<br>    this.form(req, res, function (formMarkup) {<br>        this.list(function (listMarkup) {<br>              v.render({<br>                title: "Administration",<br>                content: "Welcome to the control panel",<br>                list: listMarkup,<br>                form: formMarkup,<br>            });<br>        });<br>    });<br>});<br>const v = new View(res, "admin");<br>

      It looks a bit ugly, but it works as I wanted. The first helper is a action=delete&id=[id of the record], it removes data from the collection. The second function is called 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:

      handleFileUpload(req) {<br>    if (!req.files || !req.files.picture || !req.files.picture.name) {<br>      return req.body.currentPicture || "";<br>    }<br>    const data = fs.readFileSync(req.files.picture.path);<br>    const fileName = req.files.picture.name;<br>    const uid = crypto.randomBytes(10).toString("hex");<br>    const dir = __dirname   "/../public/uploads/"   uid;<br>    fs.mkdirSync(dir, "0777");<br>    fs.writeFileSync(dir   "/"   fileName, data);<br>    return "/uploads/"   uid   "/"   fileName;<br>  }<br>

      If a file is submitted, the node script req.files.picture. In the code snippet above, readFileSync, writeFileSync.

      Step 3. The Front-End

      The hard work is now complete. The administration panel is working, and we have a home and four records with a type of db object to the Blog, but call different /blog/:id string. This route will match URLs like 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 in its npm install command should be run in order to install the new dependencies (if any).

    • 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:

    forever start yourapp.js<br>

    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 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: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 hosts file.

    127.0.0.1   expresscompletewebsite.dev<br>

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

    # expresscompletewebsite.dev<br><virtualhost><br>    ServerName expresscompletewebsite.dev<br>    ServerAlias www.expresscompletewebsite.dev<br>    ProxyRequests off<br>    <proxy><br>        Order deny,allow<br>        Allow from all<br>    </proxy><br>    <location></location><br>        ProxyPass http://localhost:3000/<br>        ProxyPassReverse http://localhost:3000/<br>    <br></virtualhost><br>

    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:

    server {<br>    listen 80;<br>    server_name expresscompletewebsite.dev<br>    location / {<br>            proxy_pass http://127.0.0.1:3000;<br>            proxy_set_header Host $http_host;<br>    }<br>}<br>

    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.

The above is the detailed content of Build a Complete MVC Website With Express. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn