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.
Nội Dung Chính
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.
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.
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:
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:
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:
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">© 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:
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/
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.
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, https://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: