How to use EJS to template your Node.js application – LogRocket Blog

Software engineer (JS stack, GoLang incoming…) and student of computer science at the University of Lagos.

Introduction

If you are writing a backend application in Node.js and you want to send HTML back to the clients interacting with it, then you must find a way to “mix in,” or interpolate, the processed data into the HTML files you are sending.

For simple data interpolation and testing purposes, one way to do this is by concatenating HTML strings with data or using the template strings in JavaScript. But if you want to write applications with significant HTML code and dynamic content, you are better off using tools that are designed for that purpose, like template engines.

EJS (Embedded JavaScript Templating) is one of the most popular template engines for JavaScript. As the name suggests, it lets us embed JavaScript code in a template language that is then used to generate HTML.

In this article, I will walk you through a detailed guide to templating your Node application with EJS. First, we’ll cover the basics of template engines and setting up EJS, but we will move on to a more advanced guide to using EJS in your Node apps.

Template engines

According to Wikipedia’s definition, a template engine is software designed to combine templates with a data model to produce, in our case, real HTML code.

Visual representation how template engines workVisual representation how template engines work

Template engines handle the task of interpolating data into HTML code while providing some features (like partials in EJS) that would have been difficult to replicate by concatenating strings.

Introducing EJS

As mentioned earlier, EJS is one of the most popular template engines for JavaScript. One of the reasons to choose it is that EJS code looks like pure HTML.

It retains the syntax of HTML while allowing data interpolation, unlike Pug (another template engine) which uses a different syntax with indentation and spaces.

EJS files are saved with the .ejs file extension.

How to set up EJS in a Node.js application using Express

We will be using Express in this tutorial because it’s one of the best Node frameworks. It’s minimalistic and easy to get started with.

Let’s start a project from scratch. Create a new folder where you want to put the project files.

Initialize a new Node project in the folder by running npm init -y in the terminal, then to install Express and EJS, run:

npm install -S express ejs

After installation, create an app.js file and a views folder in the root folder. Inside the views folder, create two folders pages and partials; I will be explaining why we need these folders shortly.

Node app file structureNode app file structure

Now, inside views/pages folder create a file called index.ejs.

First, copy the following into app.js:

const express = require('express')
const app = express()
const port = 3000

app.set('view engine', 'ejs')

app.get('/', (req, res) => {
    res.render('pages/index')
})
app.listen(port, () => {
  console.log(`App listening at port ${port}`)
})

And the following into index.ejs:

<h1>Hi, there!</h1>

If you run node app.js on the terminal from the root folder, then visit [localhost:3000](http://localhost:3000/), you should see the following results:

Basic node app that reads "Hi, there"Basic node app that reads "Hi, there"

Now, let’s walk through some parts of the code and understand what is going on.

app.set('view engine', 'ejs') is self-explanatory. We are setting EJS as the Express app view engine. By default, Express will look inside of a views folder when resolving the template files, which is why we had to create a views folder.

In res.render('pages/index'), we are calling the render method on the response object. This renders the view provided (pages/index in this case) and sends back the rendered HTML string to the client.

Note that we didn’t have to provide the file extension because Express resolves it automatically; it knows the view engine we are using from app.set('view engine', 'ejs'). We also didn’t have to write the path as views/pages/index because the views folder is used by default.

Passing data to render

Recall that our aim is to combine data with templates. We can do that by passing a second argument to res.render. This second argument must be an object, which will be accessible in the EJS template file.

Update app.js like so:

const express = require('express')
const app = express()
const port = 3000
app.set('view engine', 'ejs')

const user = {
    firstName: 'Tim',
    lastName: 'Cook',
}
app.get('/', (req, res) => {
    res.render('pages/index', {
        user: user
    })
})
app.listen(port, () => {
  console.log(`App listening at port ${port}`)
})

Update index.ejs too:

<h1>Hi, I am <%= user.firstName  %></h1>

Run node app.js and you should get this:

Basic Node app reading "Hi I'm Tim"Basic Node app reading "Hi I'm Tim"

EJS syntax

You have just seen the basic syntax of EJS. The syntax follows as such:

<startingTag content closingTag>

This is the syntax of <%= user.firstName %>.

EJS has different tags for different purposes. This start tag <%= is called the “escape output” tag because if the string in the content has forbidden characters like > and &, the characters will be escaped (replaced by HTML codes) in the output string.

Logic in EJS

EJS lives up to its name, because we are provided with the tag <% that can contain logic (called a “scriptlet”) as its content. Any JavaScript syntax can be used in this tag.

To see this in action, update app.js:

const express = require('express')
const app = express()
const port = 3000
app.set('view engine', 'ejs')
const user = {
    firstName: 'Tim',
    lastName: 'Cook',
    admin: true,
}
app.get('/', (req, res) => {
    res.render('pages/index', {
        user
    })
})
app.listen(port, () => {
  console.log(`App listening at port ${port}`)
})

Then update index.js:

<h1>Hi, I am <%= user.firstName  %></h1>
<% if (user.admin) { %>
    <p>Let me tell you a secret: <b>I am an admin</b></p>
<% } %>

If you run the app, you will see the paragraph in the if statement displayed.
Change admin: false in the user object, and the paragraph won’t be displayed.

Take note of the syntax of the scriptlet <%*if*(user.admin) { %>. The opening { is captured between start and close tags, and the closing } in separate start and close tags.

Looping through data

Because the <% tag can contain any valid JavaScript code, we can also loop through and display data in EJS. Create a new file inside the views/pages named articles.ejs.

Next, update app.js:

const express = require('express')
const app = express()
const port = 3000
app.set('view engine', 'ejs')

const posts = [
    {title: 'Title 1', body: 'Body 1' },
    {title: 'Title 2', body: 'Body 2' },
    {title: 'Title 3', body: 'Body 3' },
    {title: 'Title 4', body: 'Body 4' },
]
const user = {
    firstName: 'Tim',
    lastName: 'Cook',
}
app.get('/', (req, res) => {
    res.render('pages/index', {
        user
    })
})
app.get('/articles', (req, res) => {
    res.render('pages/articles', {
        articles: posts
    })
})
app.listen(port, () => {
  console.log(`App listening at port ${port}`)
})

articles.ejs should also be updated like so:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Articles</title>
</head>
<body>
    <ul>
        <% articles.forEach((article)=> { %>
            <li>
                <h2>
                    <%= article.title %>
                </h2>
                <p>
                    <%= article.body %>
                </p>
            </li>
            <hr />
        <% }) %>
    </ul>
</body>
</html>

When you run the app, visit http://localhost:3000/articles and you should see the following:

Node app with empty articlesNode app with empty articles

We passed articles which is an array of post objects containing a title and a body to the articles.ejs template. Then, in the template, we loop through the array using forEach to render each post object as a list item.

Notice that the article variable that references each item of the array on each iteration of the loop <% articles.forEach((article)=> { %> is accessible in other portions of the template code until we reach the end of the closing brackets <% }) %>.

EJS partials

Some parts of websites stay the same across different pages, like the header, footer, and sidebar. EJS provides us with partials that allow us to reuse views.

Recall that we created the views/partials folder earlier. Create two new files named head.ejs and footer.ejs in this folder.

Content of head.ejs should be the following:

I have included a link to Bootstrap in head.ejs because I will be using code samples from the Bootstrap examples page.

Now, update footer.ejs like so:

</html>
<div class="container">
  <footer class="d-flex flex-wrap justify-content-between align-items-center py-3 my-4">
      <p class="col-md-4 mb-0 text-muted">&copy; 2021 Simple Blog</p>
      <ul class="nav col-md-4 justify-content-end">
          <li class="nav-item"><a href="/" class="nav-link px-2">Home</a></li>
          <li class="nav-item"><a href="/articles" class="nav-link px-2">Articles</a></li>
          <li class="nav-item"><a href="#" class="nav-link px-2">About</a></li>
      </ul>
  </footer>
</div>

And articles.ejs:

<%- include('../partials/head') %>
<body>
    <ul>
        <% articles.forEach((article)=> { %>
            <li>
                <h2>
                    <%= article.title %>
                </h2>
                <p>
                    <%= article.body %>
                </p>
            </li>
            <hr />
        <% }) %>
    </ul>
</body>
<%- include('../partials/footer') %>

I have included the head.ejs and footer.ejs partials using the include function. It takes in the relative path to the file as an argument. Because pages and partials are in the same folder, to access partials from pages, we have to first go out of the pages folder (../partials/head').

Also, take note of the EJS tag used (<%-) instead of the escaped output tag mentioned above (<%=). <%- is called the “unescaped output” tag, and is used when you want to output raw HTML.

Make sure to use it with care. Don’t use it with user input, because it can expose your application to attacks.

Let’s create another partial named header.ejs inside of views/partials. The following is the content of header.ejs:

<header class="p-3 bg-dark text-white mb-4">
    <div class="container">
        <div class="d-flex flex-wrap align-items-center justify-content-center justify-content-lg-start">
            <ul class="nav col-12 col-lg-auto me-lg-auto mb-2 justify-content-center mb-md-0">
                <li><a href="/" class="nav-link px-2 text-white">Home</a></li>
                <li><a href="/articles" class="nav-link px-2 text-white">Articles</a></li>
                <li><a href="#" class="nav-link px-2 text-white">About</a></li>
            </ul>
            <form class="col-12 col-lg-auto mb-3 mb-lg-0 me-lg-3">
                <input type="search" class="form-control form-control-dark" placeholder="Search..." aria-label="Search">
            </form>
            <div class="text-end">
                <button type="button" class="btn btn-outline-light me-2">Login</button>
                <button type="button" class="btn btn-warning">Sign-up</button>
            </div>
        </div>
    </div>
</header>

Update articles.ejs to this:

<%- include('../partials/head') %>
<%- include('../partials/header') %>
<body>
    <ul>
        <% articles.forEach((article)=> { %>
            <li>
                <h2>
                    <%= article.title %>
                </h2>
                <p>
                    <%= article.body %>
                </p>
            </li>
            <hr />
        <% }) %>
    </ul>
</body>
<%- include('../partials/footer') %>

Run node app.js , visit http://localhost:3000/articles and you should see this:

Same node app with articles as before, this time with a header and footerSame node app with articles as before, this time with a header and footer

By importing the partials, we have included the header and the bootstrap styles into the articles page. We can use those partials on other pages too.

We are also going to update index.ejs to include the partials:

<%- include('../partials/head') %>
<%- include('../partials/header') %>
<div class="container">
    <h1>Hi, I am <%= user.firstName  %> <%= user.lastName %></h1>
    <h2>Welcome to my Blog</h2>
</div>

<%- include('../partials/footer') %>

Run the app again and you can see the new index page at http://localhost:3000/

Index page for node app reading "Hi I'm Tim Cook welcome to my blog"Index page for node app reading "Hi I'm Tim Cook welcome to my blog"

Note that we can use any JavaScript operator in the EJS tags, so that we can write this instead:

...    
<h1>Hi, I am <%= user.firstName + " " + user.lastName  %> 
...

Passing data to partials

There is something wrong on the index page, can you see it?

The title of the page is Articles, because the head.ejs partial has the title of the web page hard coded as such, which is not desirable. We want the title of the page to reflect the content of the page, so we must pass in the title as an argument.

EJS makes it easy, because a partial has access to every variable in the parent view, so we just have to pass the variable in the object alongside the call to res.render.

Update the call to res.render in app.js as follows:

...
app.get('/', (req, res) => {
    res.render('pages/index', {
        user,
        title: "Home Page"
    })
})
app.get('/articles', (req, res) => {
    res.render('pages/articles', {
        articles: posts,
        title: "Articles"
    })
})
...

Then update the title tag in head.ejs:

...
    <title> 
        <%= title %>  
    </title>

...

Run the app again and each page should have the correct title.

You can also pass a variable to a partial when you include it as follows:

<%- include('../partials/head', {title :'Page Title'}) %>

Variables passed this way take precedence over variables passed through render .

Whether you choose to pass variables to a partial via the parent view or during the call to include, you must ensure that the variable is present before trying to use it in the partial. If it’s not present, then an exception will be thrown.

To avoid this, you can provide a default value for the variable in the partial, for example, we can update head.ejs as follows:

...
    <title> 
        <%= typeof title != 'undefined' ? title : 'Page Title' %>  
    </title>
...

We are using a ternary operator to check that title is not undefined , and if it is, we provide the default value Page Title.

Conclusion

In this article, we have reviewed template engines, and introduced EJS for JavaScript and how to use it. We have seen how to reuse code with partials and how we can also pass data to them.

Here is the EJS Syntax reference if you want to learn more about what’s possible with EJS.

I have intentionally left out the implementation of the About page for you to complete on your own. As a challenge, create an About page that looks like this and make sure the links work in the header and footer.

About pageAbout page

You can check out the code for this article on GitHub here.

200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, LogRocket Network Request MonitoringLogRocket Network Request Monitoringhttps://logrocket.com/signup/

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free

Share this: