Templating helps us build dynamic web applications using NodeJS and Express. In this post, we will learn about NodeJS templating with Express Handlebars.

We will use the express-handlebars package for our demo application. Basically, this package provides better integration with Express when compared to the alternative hbs package.

For your reference, there are other templating engines out there. In our previous post, we learnt templating using Express and Pug view engine.

1 – The Concept of Handlebars Templating Engine

As you might know, a templating engine consists of HTML templates with special placeholders for injecting dynamic data.

When our Express application with a templating engine support receives a request, the engine makes sure to replace the placeholders with actual data. In other words, a templating engine generates HTML at the moment of sending a response to the client.

Handlebars templating engine is primarily HTML driven. In other words, the template is built using plain old HTML. This is different from Pug where the template language is pretty different to norma HTML syntax. This distinction makes it relatively easy to work with Handlebars.

2 – Installation of NodeJS Express Handlebars

As a first step, we will install the Handlebars engine to our NodeJS Express application.

To do so, you can execute the below command:

$ npm install express express-handlebars body-parser

Below is the resultant package.json file for our application.

{
  "name": "nodejs-express-handlebars-sample",
  "version": "1.0.0",
  "description": "NodeJS Express Handlebars Sample",
  "main": "app.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app.js",
    "start-server": "node app.js"
  },
  "author": "Saurabh Dashora",
  "license": "ISC",
  "devDependencies": {
    "nodemon": "^1.18.3"
  },
  "dependencies": {
    "body-parser": "^1.18.3",
    "express": "^4.16.3",
    "express-handlebars": "3.0"
  }
}

Once the installation is complete, we can start creating some templates.

3 – Creating NodeJS Express Handlebars Template

For the purpose of our demo, we will create a simple application that is going to show a list of products. Also, we will have a page to add products to our shop and a page for 404 purpose.

Let us first create a page to display the list of products. For good code management, we will keep all the template files within the views folder of our root project directory.

See below code from the shop.hbs file.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ pageTitle }}</title>
    <link rel="stylesheet" href="/css/main.css">
    <link rel="stylesheet" href="/css/product.css">
</head>

<body>
    <header class="main-header">
        <nav class="main-header__nav">
            <ul class="main-header__item-list">
                <li class="main-header__item"><a class="active" href="/">Shop</a></li>
                <li class="main-header__item"><a href="/admin/add-product">Add Product</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <h1>My Products</h1>
        <p>List of all the products...</p>
        {{#if hasProducts }}
        <div class="grid">
            {{#each prods}}
            <article class="card product-item">
                <header class="card__header">
                    <h1 class="product__title">{{ this.title }}</h1>
                </header>
                <div class="card__image">
                    <img src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png" alt="A Book">
                </div>
                <div class="card__content">
                    <h2 class="product__price">$19.99</h2>
                    <p class="product__description">A very interesting book about so many even more interesting things!
                    </p>
                </div>
                <div class="card__actions">
                    <button class="btn">Add to Cart</button>
                </div>
            </article>
            {{/each}}
        </div>
        {{else}}
        <h1>No Products Found</h1>
        {{/if}}
    </main>
</body>

</html>

The key point to note is the placeholder syntax. In handlebars, any dynamic data can be described using the mustache syntax. For example, the placeholder <title>{{ pageTitle }}</title> means that pageTitle is a dynamic value.

Also, handlebars supports basic if-else expressions. Using an if-else condition, we render products if present and otherwise we display a simple message about no products present.

Note that handlebars does not support JavaScript expressions. Any sort of expression has to be kept within the NodeJS code. This is the reason why we use the boolean approach in the condition {{#if hasProducts }}. Here, hasProducts will contain a true or false value.

Next, we create a template file for adding products (add-product.hbs)

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ pageTile }}</title>
    <link rel="stylesheet" href="/css/main.css">
    <link rel="stylesheet" href="/css/forms.css">
    <link rel="stylesheet" href="/css/product.css">
</head>

<body>
    <header class="main-header">
        <nav class="main-header__nav">
            <ul class="main-header__item-list">
                <li class="main-header__item"><a href="/">Shop</a></li>
                <li class="main-header__item"><a class="active" href="/admin/add-product">Add Product</a></li>
            </ul>
        </nav>
    </header>

    <main>
        <form class="product-form" action="/admin/add-product" method="POST">
            <div class="form-control">
                <label for="title">Title</label>
                <input type="text" name="title" id="title">
            </div>

            <button class="btn" type="submit">Add Product</button>
        </form>
    </main>
</body>

</html>

Since there is nothing dynamic about this page, it mostly contains plain HTML to support a form with a single text field.

Lastly, we also have a template for the 404 page (404.hbs).

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ pageTitle }}</title>
    <link rel="stylesheet" href="/css/main.css">
</head>

<body>
    <header class="main-header">
        <nav class="main-header__nav">
            <ul class="main-header__item-list">
                <li class="main-header__item"><a href="/">Shop</a></li>
                <li class="main-header__item"><a href="/admin/add-product">Add Product</a></li>
            </ul>
        </nav>
    </header>
    <h1>Page Not Found!</h1>
</body>

</html>

Here also, we have simple HTML with a single placeholder for the pageTitle.

4 – NodeJS Express Handlebars Configuration

Now that our templates are ready, we can configure the templating engine to work with our application.

The configuration part is done in the app.js file of our application. See below code:

const path = require('path');

const express = require('express');
const bodyParser = require('body-parser');
const expressHbs = require('express-handlebars');

const app = express();

app.engine('hbs', expressHbs());
app.set('view engine', 'hbs');
app.set('views', 'views');

const adminData = require('./routes/admin');
const shopRoutes = require('./routes/shop');

app.use(bodyParser.urlencoded({extended: false}));
app.use(express.static(path.join(__dirname, 'public')));

app.use('/admin', adminData.routes);
app.use(shopRoutes);

app.use((req, res, next) => {
    res.render('404', {pageTitle: "Page Not Found"})
});

app.listen(3000);

To work with handlebars, we first need to register the engine. We first import the express-handlebars package. Then, we call app.engine() and register the handlebars package by calling the function expressHbs(). We give the name hbs.

Next, we set the view engine property to hbs. Also, we set the views property to views. This is basically the folder where our views are located.

Other than that, we simply add a couple of route files to our Express application context. Also, we render the 404 page by calling res.render() function. Within the function call, we supply the dynamic pageTitle variable. The data within this object is made available within the template for injection.

5 – Creating the NodeJS Express Routes

Next step is to create the routes for the product list and the add product page. We create these routes using Express Router.

Below is the code for product list (shop.js)

const path = require('path');

const express = require('express');

const rootDir = require('../util/path');
const adminData = require('./admin');

const router = express.Router();

router.get('/', (req, res, next) => {
  const products = adminData.products;
  res.render('shop', { prods: products, 
    pageTitle: 'Shop'});
});

module.exports = router;

Next, we have the add-product.js file. See below:

const path = require('path');

const express = require('express');

const rootDir = require('../util/path');

const router = express.Router();

const products = [];

router.get('/add-product', (req, res, next) => {
  res.render('add-product', { pageTitle: 'Add Product' });
}); 


router.post('/add-product', (req, res, next) => {
  products.push({ title: req.body.title });
  res.redirect('/');
});

exports.routes = router;
exports.products = products;

In both of these routers, we use the res.render() function to tell ExpressJS which template file to use and to provide the dynamic data for the same.

6 – NodeJS Express Handlebars Reusable Layout

One downside of the above approach is duplication of code.

As you can see, our HTML templates have common stuff such as CSS files and navigation headers. However, these pieces of code are duplicated in all template files. Therefore, any change to the navigation header will trigger a change in all of the template files. In other words, we are creating a maintenance problem for future development.

To get around this issue, handlebars provide the option to support reusable layout. To make use of this feature, we will create a layouts folder within the views folder. Within the new folder, we create a file main-layout.hbs.

See below:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>{{ pageTitle }}</title>
    <link rel="stylesheet" href="/css/main.css">
    {{#if formCSS}}
        <link rel="stylesheet" href="/css/forms.css">
    {{/if}}
    {{#if productCSS}}
        <link rel="stylesheet" href="/css/product.css">
    {{/if}}
</head>

<body>
    <header class="main-header">
        <nav class="main-header__nav">
            <ul class="main-header__item-list">
                <li class="main-header__item"><a class="{{#if activeShop }}active{{/if}}" href="/">Shop</a></li>
                <li class="main-header__item"><a class="{{#if activeAddProduct }}active{{/if}}" href="/admin/add-product">Add Product</a></li>
            </ul>
        </nav>
    </header>
    {{{ body }}}
</body>

</html>

As you can see, this is the common code for all our templates. However, to support various pages, we have some if-else expressions for styling and changing the active CSS class for the navigation header.

Also, we have a special placeholder {{{ body }}} for the page specific HTML code. Now we can simply change our other layout files accordingly.

First is the product list template (shop.hbs).

<main>
    <h1>My Products</h1>
    <p>List of all the products...</p>
    {{#if hasProducts }}
    <div class="grid">
        {{#each prods}}
        <article class="card product-item">
            <header class="card__header">
                <h1 class="product__title">{{ this.title }}</h1>
            </header>
            <div class="card__image">
                <img src="https://cdn.pixabay.com/photo/2016/03/31/20/51/book-1296045_960_720.png" alt="A Book">
            </div>
            <div class="card__content">
                <h2 class="product__price">$19.99</h2>
                <p class="product__description">A very interesting book about so many even more interesting things!</p>
            </div>
            <div class="card__actions">
                <button class="btn">Add to Cart</button>
            </div>
        </article>
        {{/each}}
    </div>
    {{else}}
    <h1>No Products Found</h1>
    {{/if}}
</main>

Now, we only have the product list specific HTML within the template file. All the common stuff will be taken care of by the main-layout.hbs file.

Next, we also tweak the add-product.hbs.

<main>
    <form class="product-form" action="/admin/add-product" method="POST">
        <div class="form-control">
            <label for="title">Title</label>
            <input type="text" name="title" id="title">
        </div>

        <button class="btn" type="submit">Add Product</button>
    </form>
</main>

Lastly, we have the 404 template. It will be significantly reduced.

<h1>Page Not Found!</h1>

To support the various if-else conditions in the main-layout.hbs, we need to also provide more data while rendering the template.

See below change within the shop.js.

router.get('/', (req, res, next) => {
  const products = adminData.products;
  res.render('shop', { prods: products, 
    pageTitle: 'Shop', 
    path: '/', 
    hasProducts: products.length > 0, 
    activeShop: true, 
    productCSS: true });
});

Next, we have changes in the admin.js.

router.get('/add-product', (req, res, next) => {
  res.render('add-product', {pageTitle: 'Add Product', 
    path: '/admin/add-product', 
    formsCSS: true, 
    productCSS: true, 
    activeAddProduct: true});
});

Basically, we are adding data to help handlebars evaluate the expressions and alter the HTML.

Lastly, we need to configure handlebars to read the layouts folder within the app.js. This is done by calling expressHbs() function with an object containing the path to the layout directory and other details about the layout file name and file extension.

app.engine('hbs', expressHbs({layoutsDir: 'views/layouts/', defaultLayout: 'main-layout', extname: 'hbs'}));
app.set('view engine', 'hbs');
app.set('views', 'views');

We can now run the app.

Conclusion

With this, we have successfully learnt how to perform NodeJS templating with Express Handlebars in a step-by-step manner. We first went with the inefficient approach without worrying about duplication. However, in the last section, we also made sure on how to use reusable common layouts for our template files.

The code for this post is available on Github along with the CSS files as well. Do refer to it in case of any doubts.

Want to try a different templating engine? Check out this post on NodeJS templating with Express EJS.

If you have any queries or comments about this post, please feel free to mention them in the comments section below.


Saurabh Dashora

Saurabh is a Software Architect with over 12 years of experience. He has worked on large-scale distributed systems across various domains and organizations. He is also a passionate Technical Writer and loves sharing knowledge in the community.

0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *