Introduction
In this article, we are going to take a look at how to use the Handlebars template engine with Node.js and Express. We'll cover what template engines are and how Handlebars can be used to create Server Side Rendered (SSR) web applications.
We will also discuss how to configure Handlebars with the Express.js framework and how to use built-in helpers to create dynamic pages. Finally, we will be taking a look at how to develop a custom helper when needed.
What is a Template Engine?
Back in the '90s when the internet was introduced to the world, it was mostly used for scientific purposes such as publishing research papers and as a communication channel between universities and scientists. Most of the webpages back then were static. A static web page is the same for every user and does not change on a per user basis. If anything is to be changed on a page, it would have been done manually.
In the modern world, things are much more interactive and tailored to each user. Today, almost everyone has access to the internet. Most of the web apps today are dynamic. For example, on Facebook you and I will see a very different news feeds when logged in. For each person, the page will follow the same template (i.e. sequential posts with usernames above them), but the content will be different.
This is the work of a template engine - the template for the news feed is defined and then, based on the current user and the query to the database, the template is populated with received content.
We can use template engines in both the backend and front-end. If we use a template engine in the backend to generate the HTML, we call that Server-Side Rendering (SSR).
Handlebars
Handlebars is popular for both back-end and front-end templating. For example, the popular front-end framework Ember uses Handlebars as the templating engine.
Handlebars is an extension of the Mustache template language, which is mostly focused on simplicity and minimal templating.
Using Handlebars with Node.js
To get started, create an empty folder, open the command prompt inside that folder, and then run npm init -y
to create an empty Node.js project with default settings.
Before starting, we need to install the required Node.js libraries. You can install the express and express-handlebars modules by running:
$ npm install --save express express-handlebars
Note: When using Handlebars server-side, you'll likely use a helper module like express-handlebars
that integrates Handlebars with your web framework. In this article we'll be focusing mostly on the templating syntax, which is why we're using express-handlebars
, but in case you're handling the template compilation and rendering yourself, you'll want to check out the compilation API reference as well.
Then, let's recreate the default Handlebars directory structure. The views
folder contains all Handlebars templates:
.
├── app.js
└── views
├── home.hbs
└── layouts
└── main.hbs
The layouts
folder inside the views
folder will contain the layouts or the template wrappers. Those layouts will contain the HTML structure, style sheets, and scripts that are shared between templates.
The main.hbs
file is the main layout. The home.hbs
file is an example Handlebars template that we are going to build upon.
We will add more templates and folders as we continue.
In our example we'll be using one script to keep this simple. Let's import the required libraries in our app.js
file:
const express = require('express');
const exphbs = require('express-handlebars');
Then, let's create an Express app:
const app = express();
Now, we can configure express-handlebars
as our view engine:
app.engine('hbs', exphbs({
defaultLayout: 'main',
extname: '.hbs'
}));
app.set('view engine', 'hbs');
By default, the extension for Handlebars templates is .handlebars
. But in the settings here, we've changed it to .hbs
via the extname
flag because it's shorter.
Let's include the Bootstrap scripts and styles in the main.hbs
layout:
<html lang="en">
<head>
<!-- <meta> tags> -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css">
<title>Book Face</title>
</head>
<body>
<div class="container">
{{{body}}}
</div>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
</body>
</html>
And now, let's change our home.hbs
to include a message:
<h1>Hello World from Handlebars</h1>
To be able to reach this page, we need to configure a request handler. Let's set it at the root path:
app.get('/', (req, res) => {
res.render('home');
});
Finally, we just need to start listening on a port for requests:
app.listen(3000, () => {
console.log('The web server has started on port 3000');
});
We can run the app with node app.js
in the console, though, we can also opt to use a tool like nodemon. With nodemon, we don't need to restart the server each time we make a change - when we change the code, nodemon will refresh the server.
Let's install it:
$ npm i -g nodemon
And running the app with nodemon is done via:
$ nodemon app.js
Let's visit our app via the browser:
With everything in place, let's explore some Handlebars features.
Handlebars Language Features
In order to showcase some of the Handlebars features, we'll be building a social media feed. The feed will pull data from a simple array, simulating a database.
The feed will contain posts with images and comments. If there are no comments on an image - a "Be the first to comment on this post" message will appear.
Let's update our home.hbs
to get started:
<nav class="navbar navbar-dark bg-dark">
<a class="navbar-brand" href="#">Book Face</a>
</nav>
<div class="posts">
<div class="row justify-content-center">
<div class="col-lg-7" style="margin-top: 50px;">
<div class="card">
<img src="https://picsum.photos/500/500"
class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Posted by Janith Kasun</h5>
<ul class="list-group">
<li class="list-group-item">This is supposed to be a comment</li>
<li class="list-group-item">This is supposed to be a comment</li>
</ul>
</div>
</div>
</div>
</div>
</div>
As you can see in this Handlebars template, we've have added a navbar
and a card
with some hard-coded placeholder values.
Our page now looks like this:
Check out our hands-on, practical guide to learning Git, with best-practices, industry-accepted standards, and included cheat sheet. Stop Googling Git commands and actually learn it!
Passing Parameters to Templates
Now, let's remove these hardcoded values from the page itself and pass them from the script to the page. These will later be replaced with comment values in the array:
app.get('/', function (req, res) {
res.render('home', {
post: {
author: 'Janith Kasun',
image: 'https://picsum.photos/500/500',
comments: []
}
});
});
The post
contains fields such as author
, image
, and comments
. We can reference the post
in our Handlebars template {{post}}
:
<div class="posts">
<div class="row justify-content-center">
<div class="col-lg-7" style="margin-top: 50px;">
<div class="card">
<img src="{{post.image}}"
class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Posted by {{post.author}}</h5>
<ul class="list-group">
<li class="list-group-item">This is suppose to be a comment</li>
<li class="list-group-item">This is suppose to be a comment</li>
</ul>
</div>
</div>
</div>
</div>
</div>
By referencing these values with the handler that renders the page, they're inserted on the server-side and the user is served seemingly static HTML with these values already present.
Using Conditions
Since we have conditional logic, i.e. show the comments if they're present and a message if they're not, let's see how we can use conditionals in Handlebars templates:
<div class="posts">
<div class="row justify-content-center">
<div class="col-lg-7" style="margin-top: 50px;">
<div class="card">
<img src="{{post.image}}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Posted by {{post.author}}</h5>
{{#if post.comments}}
<ul class="list-group">
<!-- Display comment logic -->
</ul>
{{else}}
<ul class="list-group">
<li class="list-group-item">Be first to comment on this post!</li>
</ul>
{{/if}}
</div>
</div>
</div>
</div>
</div>
Now, you should only see the "Be first to comment on this post" section rendered on your page since the comment array is empty:
The #if
is a built-in helper in Handlebars. If the if-statement returns true
, the block inside of the #if
block will be rendered. If false
, undefined
, null
, ""
, 0
, or []
are returned, the block won't be rendered.
Our array is empty ([]
) so the block isn't rendered.
#if
only accepts a single condition and you can't use JavaScript comparison syntax (===
). If you need to use multiple conditions or additional syntax, you can create a variable in the code and pass it down to the template. Additionally, you can define your own helper, which we'll do in the last section.
Using Loops
Since a post can contain multiple comments, we'll need a loop to get through them all and render them. Let's first populate our array with some comments:
app.get('/', function (req, res) {
res.render('home', {
post: {
author: 'Janith Kasun',
image: 'https://picsum.photos/500/500',
comments: [
'This is the first comment',
'This is the second comment',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus.'
]
}
});
});
And now, in our template, we'll use the #each
loop to go through them all:
<div class="posts">
<div class="row justify-content-center">
<div class="col-lg-7" style="margin-top: 50px;">
<div class="card">
<img src="{{post.image}}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Posted by {{post.author}}</h5>
{{#if post.comments}}
<ul class="list-group">
{{#each post.comments}}
<li class="list-group-item">{{this}}</li>
{{/each}}
</ul>
{{else}}
<ul class="list-group">
<li class="list-group-item">Be first to comment on this post</li>
</ul>
{{/if}}
</div>
</div>
</div>
</div>
</div>
Inside the #each
loop, you can use this
to reference the element that's in the current iteration. In our case, it refers to a string which is then rendered:
If you have an array of objects, you can access any attribute of that object as well. For example, if there is an array of people, you can simply use this.name
to access the name
field.
Now, let's change our template parameters to contain multiple posts:
app.get('/', function (req, res) {
res.render('home', {
posts: [
{
author: 'Janith Kasun',
image: 'https://picsum.photos/500/500',
comments: [
'This is the first comment',
'This is the second comment',
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum nec fermentum ligula. Sed vitae erat lectus.'
]
},
{
author: 'John Doe',
image: 'https://picsum.photos/500/500?2',
comments: [
]
}
]
});
});
Now we can also put an #each
to iterate through the posts:
<div class="posts">
<div class="row justify-content-center">
{{#each posts}}
<div class="col-lg-7" style="margin-top: 50px;">
<div class="card">
<img src="{{this.image}}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Posted by {{this.author}}</h5>
{{#if this.comments}}
<ul class="list-group">
{{#each this.comments}}
<li class="list-group-item">{{this}}</li>
{{/each}}
</ul>
{{else}}
<ul class="list-group">
<li class="list-group-item">Be first to comment on this post</li>
</ul>
{{/if}}
</div>
</div>
</div>
{{/each}}
</div>
</div>
Using Partial
Pretty much all web pages contain different sections. On a basic level, these are the Header, Body, and Footer sections. Since the Header and Footer are typically shared between many pages, having this in all web pages will soon become extremely annoying and simply redundant.
Thankfully, we can use Handlebars to divide these sections in templates and simply include these templates as "partials" in pages themselves.
In our case, since we don't have a footer, let's make a header.hbs
and a posts.hbs
file in a partials
directory:
.
├── app.js
└── views
├── home.hbs
├── layouts
| └── main.hbs
└── paritials
└── header.hbs
└── posts.hbs
Then, we'll move the header code into the header.hbs
file:
<nav class="navbar navbar-dark bg-dark">
<a class="navbar-brand" href="#">Book Face</a>
</nav>
And the feed code into the posts.hbs
file:
<div class="posts">
<div class="row justify-content-center">
{{#each posts}}
<div class="col-lg-7" style="margin-top: 50px;">
<div class="card">
<img src="{{this.image}}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Posted by {{this.author}}</h5>
{{#if this.comments}}
<ul class="list-group">
{{#each this.comments}}
<li class="list-group-item">{{this}}</li>
{{/each}}
</ul>
{{else}}
<ul class="list-group">
<li class="list-group-item">Be first to comment on this post</li>
</ul>
{{/if}}
</div>
</div>
</div>
{{/each}}
</div>
</div>
And now, we can include these in the home.hbs
file:
{{>header}}
{{>posts posts=posts}}
The user won't see a difference, but our home.hbs
file is much cleaner now. This becomes super useful when you've got complex web pages.
Here, we've simply included the header.hbs
file and passed a posts
parameter to the posts
field of the posts.hbs
file.
What this does is, it passes the posts
from our handler to the posts
parameter in the posts.hbs
page file.
Building a Custom Helper
As you can see on the page, we have a single comment which consumes two lines. Let's create a custom helper to summarize that text.
To do that, in the Handlebars configuration, we can define our helper functions. In our case, we'll clip comments to 64 characters:
app.engine('hbs', exphbs({
defaultLayout: 'main',
extname: '.hbs',
helpers: {
getShortComment(comment) {
if (comment.length < 64) {
return comment;
}
return comment.substring(0, 61) + '...';
}
}
}));
Now let's use this helper in our posts.hbs
template to summarize comments:
<div class="posts">
<div class="row justify-content-center">
{{#each posts}}
<div class="col-lg-7" style="margin-top: 50px;">
<div class="card">
<img src="{{this.image}}" class="card-img-top" alt="...">
<div class="card-body">
<h5 class="card-title">Posted by {{this.author}}</h5>
{{#if this.comments}}
<ul class="list-group">
{{#each this.comments}}
<li class="list-group-item">{{getShortComment this}}</li>
{{/each}}
</ul>
{{else}}
<ul class="list-group">
<li class="list-group-item">Be first to comment on this post</li>
</ul>
{{/if}}
</div>
</div>
</div>
{{/each}}
</div>
</div>
Surely enough, the comments are clipped on our page now:
Conclusion
In this article, we covered the basics of Handlebars - a templating engine for Node.js and front-end JavaScript. Using Handlebars, we can create dynamic webpages that render on the server side or client side. Using Handlebars' conditionals, loops, partials, and custom helper functions, our web pages become more than just static HTML.
The code is also available on GitHub, as usual. You can also find more info on Handlebars at their official webpage.