Templating a Node.js app with EJS

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:

  1. Focus inside your TERMINAL and click Ctrl+c, this will stop the server
  2. Type node index, this will restart the server
  3. 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.
Replace
res.send("Hello World!")
by
res.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:

  1. The head
  2. The body
  3. 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:

  1. We can create a file for this line and include it at the desired place, just like head.ejs and footer.ejs.
  2. Or we can put the line just after the opening <body> tag at the end of head.ejs, since this <h1> (article title) appears directly after including head.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:

  1. The code father 🤣
  2. 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:

  1. Having a stylesheet in our app and load it in the <head> of the document.
  2. 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:

  1. We didn't add in the <head> of the document the link that connects this stylesheet, main.css, to the document itself.
  2. 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():

  1. 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') %>
  2. 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:

  1. If the file is directly inside the public folder, we just call it with it's extension.
    Let's say that the-code-father.jpg was directly inside the public folder and we wanted to call it inside index.ejs, we would write:
    <img src="the-code-father.jpg" />
  2. 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 💘

Note: The background image is a photo by Sen.

SYA,
LebCit.