This post is about templating a Node.js application with EJS.
Intro
There are a lot of tutorials about EJS out there, as well as a lot of
questions/answers.
I
decided to make my own as a beginner's guide, full of details, to bring the reader's
attention about what could be going on or maybe off...
In this post, we are going to build a basic Node.js web application using EJS as a
templating language.
So, we'll go through the process step by step and explain
every step to understand it 🧠
Template engines
A template engine, processor or parser, is a software designed to inject data into a file, that commonly have the same extension as the template language or refers to, by using special markups that the template engine is programmed to understand and execute, resulting in a document where data have been passed from one end to another.
Why don't we put the desired data directly into the file?
Good question!
The
simple answer is that template engines where created not only to structure the output of a
document, but also to pass, get and set dynamic data as numbers, dates, strings, database
information and so on...
Today, template engines are mostly used to build server-side applications structured as fragments, using a template language to combine those small pieces and pass data across them to rapidly produce a ready to use application.
Some popular JavaScript template engines:
- EJS: Embedded JavaScript templating
- Pug: Robust, elegant, feature rich template engine for Node.js
- Handlebars: Minimal templating on steroids
- Nunjucks: A rich and powerful templating language for JavaScript
- Mustache: Logic-less {{mustache}} templates with JavaScript
I prefer and recommend EJS because:
It's just plain JavaScript.
Setup
To follow up this tutorial, you'll need to install on your machine
Node.js, always go for the Long Time Support version,
at the time of writing this post it's 16.15.1 LTS.
You'll also need a code
editor, I use and strongly recommend VS Code,
the best and most developer friendly code editor in my opinion.
Now, create a folder named ejs where you usually stock your projects, mine is simply
projects.
Open this folder in VS Code, then open the
TERMINAL by
pressing on 'Ctrl' and '`' (the backtick character under Esc) at the same
time.
Type the following command to initialize a project using
npm (Node Package Manager):
npm init -y
This command will create a package.json
file at the root of your ejs project.
This JSON
file contains all the information about your project.
You can
add a description
, modify the version
's app as well as the
main
entry file of the app (default is index.js
), add
scripts
, keywords
, your name as the author
, and keep
track of installed packages (we'll see this last one in a moment).
You can leave it
as it is, and it's also fine, since we are just testing and not publishing our app.
Express
Since we're building a web app in Node.js, we'll be using Express to simplify our development and take advantage of it's features:
- Fast server-side development: Time saver, by using features of Node.js as functions
- Middleware: Access application's request-response cycle
- Routing: Great routing mechanism for client requests
- Templating: Build dynamic content with template engines
- Debugging: Express points to the part where a bug occurs
Express is the most popular Node.js web framework, written in plain JavaScript, that helps
developers build web and mobile applications quickly with ease.
Express let's also
developers build the way they want, without forcing them into a specific way of doing
things, this is why it's called unopinionated!
Install Express by typing the following command in the TERMINAL:
npm i express
# i stands for install
This command will install Express into the application.
You'll see after running
this command a node_modules
folder as well as a
package-lock.json
file at the root of the app.
We now have to display the famous Hello World! with Express.
The main entry
file, if you didn't change it in package.json
, is index.js
,
this file is commonly known as the main server file.
Create this file at the root of
the app.
The app structure should now look like the following tree:
Application's root without node_modules folder
├── index.js
├── package-lock.json
└── package.json
Put the following code in index.js
:
/index.js
const express = require("express")
const app = express()
app.get("/", (req, res) => {
res.send("Hello World!")
})
// Launching the app on port 3000
app.listen(3000, () => {
console.log(`App launched @ http://localhost:3000`)
})
To run this app, simply type in the TERMINAL the following command:
node index
You'll immediately see under it:
App launched @ http://localhost:3000
Put your mouse over the link and click on Follow link.
If you
don't see it, put your mouse over the link, press Ctrl
and click on the
link.
You now should see a beautiful Hello World! in your default browser!
Nodemon
Remember that this app is a server-side one!
Brief and quick example.
Try to
change Hello World! in index.js
to whatever other string, let's
say Hi from server-side! and save the file.
If you reload your browser nothing
happens, no change is made!
This is because the server is still running up your
previous code.
To display your new string, you should stop the server, restart it, then
reload the browser.
You can do the following in the TERMINAL:
- Focus inside your TERMINAL and click
Ctrl+c
, this will stop the server - Type
node index
, this will restart the server - Reload your browser and your new string appears
When developing, you'll inspect your changes in the browser as much as you're coding, the above process to see the new output is unproductive, a time killer, and will make your hate coding.
Fortunately, their is a very popular and well maintained package called
nodemon that takes care of automatically
restarting the Node.js application when changes are detected for a .js
,
.mjs
or .json
file in the working directory.
Nodemon is
configurable and will allow us to define which directories and extensions to watch or even
ignore, and that's just great!
Let's install Nodemon. In the TERMINAL, type the following command:
npm i -D nodemon
# -D is a flag standing for development
This command will install Nodemon as a development package, this means that we need this
package only for development and not for production/deployment.
Open
package.json
, remove test
command under scripts
and
replace it by the following one:
"start": "nodemon index.js"
The file should now look like this:
/package.json
{
"name": "ejs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon index.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"express": "^4.18.1"
},
"devDependencies": {
"nodemon": "^2.0.19"
}
}
Now, all you have to do is to stop the server if it's running then type in the TERMINAL:
npm start
You'll get directly under it:
> ejs@1.0.0 start
> nodemon index.js
[nodemon] 2.0.19
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node index.js`
App 🚀 @ http://localhost:3000
Yes, I've replaced launched by 🚀 in index.js
.
Follow the
link.
Try now to change Hi from server-side! to anything else, save the file
and then reload your browser.
Great! The server have restarted the app and you can now
see your changes, after you have saved them, by reloading the browser.
First EJS file
To start using EJS in Node.js, we have first to install it, you should by now know where to type the following command:
npm i ejs
Update index.js
to set
EJS as our template engine:
/index.js
const express = require("express")
const app = express()
// Set EJS as template engine
app.set("view engine", "ejs")
app.get("/", (req, res) => {
res.send("Hello World!")
})
// Launching the app on port 3000
app.listen(3000, () => {
console.log(`App 🚀 @ http://localhost:3000`)
})
Since EJS is now installed, let's use it!
To do so, we have to create a
views
folder at the root of our app.
Express will look inside this folder
to render
our page(s) by using EJS as a template engine.
Inside
views
, create an index.ejs
file.
The app structure should now
look like the following tree:
Application's root without node_modules folder
├── index.js
├── package-lock.json
├── package.json
└── views
└── index.ejs
Put the following content inside index.ejs
:
/views/index.ejs
<!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>EJS App Homepage</title>
</head>
<body>
<h1>Hello from EJS</h1>
</body>
</html>
As you can see, an .ejs
file is just HTML
in which we can use
plain JavaScript
, and we'll do that later on.
Now, all we have to do to render
this template is to modify only one line in
index.js
.
Replaceres.send("Hello World!")
byres.render("index")
No need to specify the extension
of the file because Express has been told to use EJS as a template engine.
If your server is still running, just reload your browser, otherwise restart it and follow
the link.
Congrats, your first EJS file is rendered!
You can try to modify it,
save your modification(s), and refresh your browser to see the changes.
LiveReload
Earlier, we've solved a half of the problem, no need to shut down the server and restart
it anymore.
But what about the browser, should I always refresh it?
Absolutely
not! This would also be an insane development step to do in today's world!
Imagine
having to refresh each and every time you make some changes...
Let's bring into our project 2 packages that will do the work for us.
Type the
following command:
npm i -D livereload connect-livereload
This command will install
livereload and
connect-livereload as
development dependencies, just like Nodemon.
As you can guest, they work together,
along with Nodemon, to reload the browser when files on the server have been modified.
Let's implement this behavior by updating index.js
:
/index.js
const express = require("express")
const app = express()
// Set EJS as template engine
app.set("view engine", "ejs")
// Require livereload and connectLiveReload
const livereload = require("livereload")
const connectLiveReload = require("connect-livereload")
// Create a server with livereload and fire it up
const liveReloadServer = livereload.createServer()
// Refresh the browser after each saved change on the server with a delay of 100 ms
liveReloadServer.server.once("connection", () => {
setTimeout(() => {
liveReloadServer.refresh("/")
}, 100)
})
// Add livereload script to the response
app.use(connectLiveReload())
// Render the main route with EJS
app.get("/", (req, res) => {
res.render("index")
})
// Launching the app on port 3000
app.listen(3000, () => {
console.log(`App 🚀 @ http://localhost:3000`)
})
A final step is needed for this to work as expected.
We'll have to add a command
under scripts
in package.json
:
/package.json
{
"name": "ejs",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "nodemon index.js",
"watch": "nodemon -e js,ejs" // Added command
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"ejs": "^3.1.8",
"express": "^4.18.1"
},
"devDependencies": {
"connect-livereload": "^0.6.1",
"livereload": "^0.9.3",
"nodemon": "^2.0.19"
}
}
This command tells Nodemon to watch for changes of files with a .js
or
.ejs
extension in the app.
Nota bene: you can change the name of
the command to whatever you like!
So for example you could rename start to
fire and maybe watch to build...
Now comes the moment of truth.
If the server is running stop it.
Type the
following command:
npm run watch
If you follow the link, you should see a big Hello from EJS.
Now, try to
change it to whatever you like and save.
Abracadabra, the browser
reloads automatically and your changes are rendered to the browser instantly!
Templating with EJS
Templating is the mechanism of combining multiple parts into one.
In the
views
folder, create a new folder called partials
.
This
folder will contain the different parts of our app.
As you know, the main parts of a
webpage are:
- The head
- The body
- The footer
Sometimes, there is also a sidebar, but it's generally a part of the body.
Now, create 2 files inside the partials
folder:head.ejs
and
footer.ejs
.
/views/partials/head.ejs
<!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>EJS App Homepage</title>
</head>
<body></body>
</html>
/views/partials/footer.ejs
</body>
</html>
Man, your code is a mess!
There is no closing tags for html
and
body
in head.ejs
, moreover there is two lonely closing tags for
html
and body
in footer.ejs
!
I know, be patient,
the magic is coming.
Update index.ejs
with the following content:
/views/index.ejs
<%- include('partials/head') %>
<h1>I'm templating with EJS</h1>
<%- include('partials/footer') %>
Save and go take a look at your browser 😁
The code is self explanatory.
We are
using the include()
function of EJS, to include a template
into another at a specific location.
This is a
HUGE step in your dev journey, trust me!
Ok, it's very nice, but what's the benefit since we could put it all together?!
🤔
Good question!
The first obvious benefit is to write a
DRY (Don't Repeat Yourself) code!
If you have ever built a static
website before, you'll know that you would include the header, maybe a sidebar, and the
footer for each and every page.
Well, this will not be the case anymore 😉
The
second and most amazing benefit is that you'll be able to write dynamic code by using
JavaScript inside the template, you already did it with include()
, but
we'll see much more in the next section.
Passing data
This is, in my opinion, the most powerful, used and sensitive functionality of any template
engine!
Although, we should be aware of how to use it and what we are passing from one
file to another and especially from one end (the backend/server) to another (the
frontend/browser)!
Simple routing
Let's begin by adding another page to our app.
Create an
about.ejs
file in the views
folder:
/views/about.ejs
<%- include('partials/head') %>
<h1>About me</h1>
<p>
I'm LebCit, a Citizen of a small country called
<a href="https://www.google.com/maps/@33.8735578,35.84741,9z">Lebanon</a>
in the Middle East.
</p>
<%- include('partials/footer') %>
While your server is running up, try to go to the following route:
http://localhost:3000/about
You will see an error
page with the following message:
Cannot GET /about
This is because the /about
route was not defined in the backend.
Open
index.js
and add under the main route a new one:
// Rendering about page on the /about route
app.get("/about", (req, res) => {
res.render("about")
})
Go back to your browser and refresh it.
Why the browser did not refresh itself?
Your
server did reload the app, but your browser is on an error
page and
livereload
is now blocked, so even if you make changes they will not be
reflected on the frontend.
When you refresh your browser, you'll see the content of
about.ejs
.
Simple exercise: try to render
about.ejs
on
about-me
route.
Data object
If you look closer to the browser's title when you're viewing the content of
about.ejs
, you'll notice that it's the same one as
index.ejs
: EJS App Homepage
This is because they share the same
<title>
passed from head.ejs
!
The
<title>
is intended to announce the content of a page, so our pages
cannot have the same <title>
since they provide different content.
In
a real life situation, an app will have a different <title>
for each page
rendered on a route.
To give our pages different titles, we'll use the data object
of EJS on a
rendered page.
First of all, make sure that you're viewing
about.ejs
in your browser.
Modify the code that renders it in
index.js
to:
/**
* Rendering about page on the /about route,
* while passing the document title as a data object.
*/
app.get("/about", (req, res) => {
res.render("about", { docTitle: "About me | EJS App" })
})
In head.ejs
, modify the <title>
line to:
<title><%= docTitle %></title>
We are passing the docTitle
value About me | EJS App
from the
backend to the frontend as a variable for a rendered page on a route into the
<title>
tag.
Save both files and go take a look at the browser's
title 😁
Now, go to the homepage at http://localhost:3000/
!!!
Don't panic and read: this is a crucial rule if you ever want to be a developer when you see an error!
Express
is amazing, it's showing you the exact position where the error occurred, as well as the
error itself: docTitle is not defined
. Remember that in
index.ejs
we are including head.ejs
.
Of course! In
head.ejs
there is a variable called docTitle
, but we didn't
pass it into the page index.ejs
rendered on the main route!
To do so,
modify the main route in index.js
to:
/**
* Rendering index page on the main route,
* while passing the document title as a data object.
*/
app.get("/", (req, res) => {
res.render("index", { docTitle: "Homepage | EJS App" })
})
Save the file and refresh your browser.
You now know why you have to refresh your browser in this case!
Switch between the homepage and the about page, they are displaying different titles!
Great job!
Don't Repeat Yourself
Suppose the <h1>
tags in index.ejs
and
about.ejs
represent titles of the content in each page, like an article title,
just a little bit more descriptive than the document title.
Let's render them by
using the data object:
app.get("/", (req, res) => {
res.render("index", { docTitle: "Homepage | EJS App", pageTitle: "Homepage of templating with EJS application" })
})
app.get("/about", (req, res) => {
res.render("about", { docTitle: "About me | EJS App", pageTitle: "A little bit about myself" })
})
And of course in each file the <h1>
line becomes:<h1><%= pageTitle %></h1>
Suppose now that we have in our app just 50 articles, so we will have to write at the very
beginning of each blog post the previous line of code.
We would be misusing the
template engine, since one of it's benefits is to write DRY code!
To solve this:
-
We can create a file for this line and include it at the desired place, just like
head.ejs
andfooter.ejs
. -
Or we can put the line just after the opening
<body>
tag at the end ofhead.ejs
, since this<h1>
(article title) appears directly after includinghead.ejs
in both files.
In this particular case, I prefer the second approach because it's just one line and not
a partial or a huge block of code to be repeated.
The file would look like:
/views/partials/head.ejs
<!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><%= docTitle %></title>
</head>
<body>
<h1><%= pageTitle %></h1>
</body>
</html>
Now you have a little idea about passing data from the backend to the frontend as well as one or two benefits a template engine can provide.
Navigation
A user can't guess the different routes of an application. An app should help the user
to explore and use it's content by providing a way to navigate between it's
different routes.
This is where the navigation comes in.
Create a nav.ejs
in partials.ejs
with the following content:
/views/partials/nav.ejs
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About me</a></li>
</ul>
</nav>
Pay attention, the href
attributes are the same as the defined routes on the
server!
Now, let's include this part in head.ejs
after the opening
<body>
tag and before the <h1>
:
/views/partials/head.ejs
<!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><%= docTitle %></title>
</head>
<body>
<%- include('nav') %>
<h1><%= pageTitle %></h1>
</body>
</html>
Save both files and take a look at your browser.
You should have a working navigation
above the page title.
Static files
For now, we've been rendering .ejs
files, including them one into another,
and passing some basic data from the backend to the frontend using the
data object
of EJS.
All of this is great but does not put you on the right track by itself to build an entire
app.
You'll quickly need to insert images, style the page(s) with
CSS, add some interactivity
with JavaScript...
As I told you before, Express is amazing!
One line inside the main server file
is all we need to start serving static files.
Update index.js
by putting
the magical line just bellow the template engine:
...
// Set EJS as template engine
app.set("view engine", "ejs")
// Serve static files from a folder named public
app.use(express.static('public'))
...
Now, create a public
folder at the root of your application.
Remember that
this folder will hold files intended to be rendered directly in the browser/frontend.
Images
Let's begin by creating an img
folder inside the
public
folder.
I'll be getting 2 images from
Pexels:
- The code father 🤣
- Byblos one of the oldest cities in the world
I've downloaded the smallest available size for each of them, and renamed them to
the-code-father.jpg
and byblos.jpg
.
I'll be including
the-code-father.jpg
into index.ejs
:
<%- include('partials/head') %>
<img src="img/the-code-father.jpg" />
<%- include('partials/footer') %>
Now it's your turn to insert byblos.jpg
into about.ejs
.
Styles
We can load styles into a page by:
-
Having a stylesheet in our app and load it in the
<head>
of the document. -
Load a stylesheet in the
<head>
of the document from a CDN, Content Delivery Network, like JSDELIVR.
The same logic applies for fonts.
Create a css
folder under the public
one.
Inside the
css
folder, create a file called main.css
.
Basic definition:
CSS is used to design the display of webpages.
Let's change the color of the page
title to blue, by adding the following in main.css
:
h1 {
color: blue;
}
If you go to your browser, nothing has changed and in fact it didn't reload for 2 reasons:
-
We didn't add in the
<head>
of the document the link that connects this stylesheet,main.css
, to the document itself. -
We didn't instruct Nodemon to reload the app nor the browser when
.css
files have been changed.
Open head.ejs
and add the link that injects main.css
into the
document:
...
<title><%= docTitle %></title>
<!-- Load main.css stylesheet -->
<link rel="stylesheet" href="css/main.css" />
...
If you take a look at your browser, the <h1>
tags are now blue.
Wait
we didn't instruct Nodemon to watch .css
files!
That's right, but
it's watching .ejs
files, where a change have been made.
Try to change blue
to red
in main.css
.
Look at
your browser, <h1>
tags are still in blue.
Open
package.json
and update the watch
command to:"watch": "nodemon -e js,ejs,css"
We are now watching for changes in .css
files also.
Stop the server
and restart it again, <h1>
tags are now red.
Change red to any color
you want and the changes will now by reflect on the frontend.
This is really nice, but for an app you'll need some ready to use styles to save time
and fasten your development.
This is where CSS frameworks come in.
There are a lot
of CSS frameworks out there, I'll be using Pure.css:
A set of small, responsive CSS modules that you can use in every web project.
To load Pure.css
I'll be using JSDELIVR:
<!-- Load main.css stylesheet -->
<link rel="stylesheet" href="css/main.css" />
<!-- Load Pure.css stylesheet -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/purecss@2.1.0/build/pure-min.min.css" />
If you look at your browser, you'll notice some minor changes but nothing much.
To
see it in action, modify nav.ejs
:
/views/partials/nav.ejs
<nav class="pure-menu pure-menu-horizontal">
<ul class="pure-menu-list">
<li class="pure-menu-item"><a href="/">Home</a></li>
<li class="pure-menu-item"></li><a href="/about">About me</a></li>
</ul>
</nav>
Save and take a look at your browser.
Congrats, you've just used pre-made styles
from a loaded framework.
Read the framework's documentation and play with it's
classes to style your page(s).
You can also take a look at the
Layouts that Pure.css
provides to
have a better understanding of how you can use it.
JavaScript
There is no simple definition for JavaScript
...
Look at
the frontend technology used in most popular websites.
Check also Stack Overflow's 2021 Developer Survey regarding
Programming, scripting, and markup languages.
In today's world, web development is mostly about
JavaScript
.
You have been using it all along from the beginning of this
tutorial!
In index.ejs
add a <button>
inside a
<div>
under the <img />
:
<div>
<button id="btn" class="pure-button">A Button</button>
</div>
Create a scripts
folder in public
.
Inside
scripts
, create a file named main.js
and add the following content
inside it:
/public/scripts/main.js
const btn = document.getElementById("btn")
if (btn) {
btn.addEventListener("click", () => {
alert("Hello from JavaScript!")
})
}
Just like a stylesheet, a script file should be loaded into the document to work.
It's
safer, and best practice, to load scripts at the very end of a document.
Modify
footer.ejs
to look like:
<script src="scripts/main.js"></script>
</body>
</html>
If you visit the homepage of the app and click on the button, you'll see a popup with
the message defined in main.js
.JavaScript
can do much more
than simple interactions, you can control and modify everything in a document...
There is a countless number of JavaScript frameworks out there to make your development easier and faster or make you hate it, so be very careful of what you're using and always ask yourself if you really need it or not!
A personal note about JavaScript: I love vanilla JS
Meaning that I'm not a fan of frontend JS frameworks, but that's just me...
Paths
Important subsection ahead!
You may have noticed or not, but you where using relative paths to
include()
templates in each other and load assets into the document.
Consider the tree of our app:
Application's root without node_modules folder
├── index.js
├── package-lock.json
├── package.json
├── public
│ ├── css
│ │ └── main.css
│ ├── img
│ │ ├── byblos.jpg
│ │ └── the-code-father.jpg
│ └── scripts
│ └── main.js
└── views
├── about.ejs
├── index.ejs
└── partials
├── footer.ejs
├── head.ejs
└── nav.ejs
The root is the ejs
folder.
If 2 or more files lives in the same folder,
we say that they are at the same level
.
Sometimes, we need to call a file into another, this is where we should be careful about the
process.
Always read the documentation of the tools
you are using.
If we where developing without backend technologies, and wanted to load
main.css
in index.ejs
, we would write:
<link rel="stylesheet" href="../../public/css/main.css" />
Since we where using Express and EJS as a defined template engine, we where able to use relative paths to call our files.
⚠️ Never begin with the views
folder when templating, he's already detected
by Express!!!
When using include()
:
-
If two files are at the same level and we wanted to
include()
one of them into another, we just type the file name, no need for the extension, like we did with:<%- include('nav') %>
-
Otherwise, we navigate to the desired file by typing the folder(s) name(s) where the
file is, then the file name without it's extension, like we did for:
<%- include('partials/head') %>
and<%- include('partials/footer') %>
⚠️ Never begin with the public
folder when using it in a template, he's
already detected by Express!!!
When calling files from public
:
-
If the file is directly inside the
public
folder, we just call it with it's extension.
Let's say thatthe-code-father.jpg
was directly inside the public folder and we wanted to call it insideindex.ejs
, we would write:<img src="the-code-father.jpg" />
-
Otherwise, we navigate to the desired file by typing the folder(s) name(s) where the
file is, then the file name with it's extension, like we did
for:
<img src="img/the-code-father.jpg" />
Conclusion
I hope that this tutorial will help a lot of new developers to start using EJS as a template engine in Node.js and understand how things are related together.
Next one will be about improving this app 😉
This tutorial was specially made for my beloved son 💘