Skip to content

Views in Express.js Explained – Static vs Dynamic Views

A view is a template or document that defines the content and structure of a webpage returned to users by an Express.js application. It can be a static HTML file or a dynamic template rendered into HTML at runtime by a template engine.

In this guide, we’ll use a simple project to learn how Express views work. There are four main steps:

  1. Create the views.
  2. Listen for client requests.
  3. Analyze the client’s request.
  4. Respond by serving the appropriate rendered view to the client.

Let’s get started by opening your terminal and making a new directory for your project.

Use the mkdir command to create your project directory:

Terminal window
mkdir codesweetly-expressjs-views-guide-001

Navigate to the new project directory using your command line.

Terminal window
cd path/to/codesweetly-expressjs-views-guide-001

Once you’re in the project folder, use NPM to create a package.json file.

Terminal window
npm init -y

Next, open the package.json file and delete the main field.

Configure the Project as an ES Module Application

Section titled “Configure the Project as an ES Module Application”

This guide uses ES Modules in all JavaScript files, so update the type field in your project’s package.json file to module.

package.json
{
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"type": "module"
}
Terminal window
npm install express

There are two types of views in Express.js:

  • Static Views
  • Dynamic Views

Static views contain the final HTML that the server sends to the browser as-is. For example, let’s configure your Express.js application to serve four static views.

Create index.html, about.html, contact.html, and 404.html files in your project’s root directory.

Terminal window
touch index.html about.html contact.html 404.html

Open each view and add the following HTML content:

index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Static Views in Express.js | CodeSweetly Tutorial</title>
</head>
<body>
<h1>Welcome to the Static View Guide</h1>
<p>Site's pages:</p>
<ul>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</body>
</html>
about.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>About Us | Static Views in Express.js</title>
</head>
<body>
<h1>About Us</h1>
<p>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Libero soluta
voluptas reprehenderit minus veniam! Corrupti a esse quidem nostrum harum,
explicabo tempore tempora aut, sint et voluptatem magni ea vel?
</p>
<div><a href="/">Return to the home page</a></div>
</body>
</html>
contact.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Contact | Static Views in Express.js</title>
</head>
<body>
<h1>Contact Us</h1>
<ul>
<li><a href="https://codesweetly.com">Website</a></li>
<li><a href="https://x.com/oluwatobiss">X (Twitter)</a></li>
</ul>
<div><a href="/">Return to the home page</a></div>
</body>
</html>
404.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>404 - Page not found | Static Views in Express.js</title>
</head>
<body>
<h1>Page not found</h1>
<div>Sorry, we couldn't find that page</div>
<div><a href="/">Go back to the home page</a></div>
</body>
</html>

Next, build a web server for your project to handle browser connections.

Configure a web server to serve static views

Section titled “Configure a web server to serve static views”

Create an index-001.js file that sets up your app to handle client requests.

Terminal window
touch index-001.js

Open the module and configure the web server to serve the appropriate static HTML view in response to requests.

index-001.js
// Add the required modules
import express from "express";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
// Create a new Express application instance
const app = express();
// Specify the hostname and port to receive requests
const host = "localhost";
const port = 3000;
// Get the directory name of the current file's path
const __dirname = dirname(fileURLToPath(import.meta.url));
// Use the index.html static view as a response to 'GET /' requests
app.get("/", (req, res) => res.sendFile(join(__dirname, "index.html")));
// Use the about.html static view as a response to 'GET /about' requests
app.get("/about", (req, res) => {
res.sendFile(join(__dirname, "about.html"));
});
// Use the contact.html static view as a response to 'GET /contact' requests
app.get("/contact", (req, res) => {
res.sendFile(join(__dirname, "contact.html"));
});
// Use the 404.html static view as a response to 404 requests
app.use((req, res) => {
res.status(404).sendFile(join(__dirname, "404.html"));
});
// Run the server on the specified port and hostname
app.listen(port, host, () => {
console.log(`Server running live at http://${host}:${port}`);
});

Here are the main things the snippet above does:

  • Create an Express application instance to access Express APIs, such as routers and middleware, which simplify HTTP request handling in Node.js.
  • Get the directory name of the current file’s path.
  • Create routes to receive and respond to clients’ requests.
    • app is an instance of the Express server.
    • The app.get() Express.js route defines how the server handles HTTP GET requests to the specified path (the method’s first argument).
    • The res.sendFile() method sends a file from the server to the client.
    • join() joins multiple path segments into a single, normalized, cross-platform compatible path string.
    • The app.use() method lets you set up handler functions that Express runs for all HTTP request methods and paths. If you specify a path argument, app.use() treats it as a prefix for matching routes. For example, /book matches /book, /book/dashboard, and /book/dashboard/author/202605.
    • res.status(404) sets an HTTP error status code for the wildcard path’s response.
  • Use Express’s app.listen() method to start the Node.js HTTP server on a specific port and execute a callback when the server begins listening for client requests.

Once you’ve set up the server, run it with Node.js.

Terminal window
node --watch index-001.js

Afterward, check your app running live at http://localhost:3000.

To keep things organized, put all your static views in a public directory. This is a common practice.

Create a public directory at your project’s root to store all static views.

Terminal window
mkdir public

Move all static views into the public folder

Section titled “Move all static views into the public folder”
Terminal window
mv index.html about.html contact.html 404.html public/

Update the server to retrieve static views from the public folder

Section titled “Update the server to retrieve static views from the public folder”
index-001.js
import express from "express";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const app = express();
const host = "localhost";
const port = 3000;
const __dirname = dirname(fileURLToPath(import.meta.url));
// Serve static assets (HTML, CSS, images) from the 'public' folder
app.use(express.static(join(__dirname, "public")));
// Use the index.html static view as a response to 'GET /' requests
app.get("/", (req, res) => {
res.sendFile(join(__dirname, "/public/index.html"));
});
// Use the about.html static view as a response to 'GET /about' requests
app.get("/about", (req, res) => {
res.sendFile(join(__dirname, "/public/about.html"));
});
// Use the contact.html static view as a response to 'GET /contact' requests
app.get("/contact", (req, res) => {
res.sendFile(join(__dirname, "/public/contact.html"));
});
// Use the 404.html static view as a response to 404 requests
app.use((req, res) => {
res.status(404).sendFile(join(__dirname, "/public/404.html"));
});
// Run the server on the specified port and hostname:
app.listen(port, host, () => {
console.log(`Server running live at http://${host}:${port}`);
});

Let’s now discuss dynamic views.

Dynamic views contain placeholders that template engines, such as EJS, replace with data at runtime.

The most popular Express.js template engines are EJS, Pug, and Handlebars. Let’s use EJS as an example to discuss how dynamic views work.

EJS (Embedded JavaScript templating) is a templating language that lets you create dynamic HTML views by embedding JavaScript code within HTML templates using template tags.

When the server renders a template, the EJS engine executes the embedded JavaScript and generates the final HTML sent to the client.

For instance, consider the following code:

// Add the Express and EJS modules
import express from "express";
import ejs from "ejs";
// Create a new Express application instance
const app = express();
// Specify the host and port to receive requests
const host = "localhost";
const port = 8000;
// Define the data object
const friendsArray = ["Sarah", "Abraham", "Mary"];
const replacementObject = { friends: friendsArray };
// Use backticks to define a template string with embedded JavaScript
const templateString = `
<h1>Hello, <%= friends.join(", "); %>!</h1>
<p>Welcome to CodeSweetly.</p>
`;
// Compile and render the template string to HTML (the rendered view)
const html = ejs.render(templateString, replacementObject);
// Use the dynamic view as a response to 'GET /' requests
app.get("/", (req, res) => res.send(html));
// Run the server on the specified port and hostname:
app.listen(port, host, () => {
console.log(`Server running live at http://${host}:${port}!`);
});

The above snippet tells Express to send the rendered view to the client when users request the / path.

At runtime, EJS executes the embedded JavaScript code (friends.join(", ")) and replaces each friends instance in the EJS template tag with friendsArray’s value.

EJS template tags are special tags written using the <% ... %> syntax. They allow JavaScript code and values to be embedded into EJS view templates.

When EJS renders the template, it executes the JavaScript inside these tags to generate HTML.

For example, the <%= %> tags below allow you to embed a JavaScript friend variable into the view template.

<h1>About <%= friend %>, my special pal</h1>

The nine (9) types of EJS template tags are as follows:

Use the closing tag to end an EJS tag. It has no special behavior on its own.

Use the scriptlet tag to run JavaScript code without outputting anything into the generated HTML. This tag is commonly used for control-flow, such as if, for, and forEach logic.

<ul>
<% friends.forEach(function () { %>
<li>My friend</li>
<% }) %>
</ul>

The snippet above wrapped <% %> around the control-flow syntax to indicate to EJS that the JavaScript code is only for logic and should not be rendered to the generated HTML.

Use the escaped output tag to evaluate JavaScript expressions and output the result into the generated HTML while escaping HTML characters. This tag helps prevent HTML injection.

<%= 100 + 200 %>

The above snippet wrapped <%= %> around the arithmetic expression to tell EJS to render the code’s value into the generated HTML.

Use the unescaped tag to evaluate JavaScript expressions and output the result into the generated HTML without escaping HTML characters.

<%- "<h1>About CodeSweetly</h1>" %>

The snippet above wrapped <%- %> around the H1 element to tell EJS to render the expression without escaping the HTML elements.

Use the literal tag to output a literal <% sequence instead of treating it as an EJS tag. This is useful when you want to show EJS syntax in the generated output.

<%% "<p><strong>Name:</strong> <em>Oluwatobi</em></p>" %>

The above snippet wrapped <%% %> around the paragraph element to tell EJS to render the expression’s EJS and HTML syntax. So, the HTML generated will look as follows:

<% "
<p><strong>Name:</strong> <em>Oluwatobi</em></p>
" %>

Use the Whitespace-trimmed ending tag to remove the newline immediately after the closing tag.

<ul>
<% friends.forEach(function (friend) { %>
<li><%= friend -%></li>
<% }) %>
</ul>

The snippet above wrapped <%= -%> around the friend variable to tell EJS to trim any newline following the closing EJS tag.

Use the whitespace slurping tag to remove whitespace before the opening EJS tag.

<ul>
<%_ friends.forEach(function (friend) { %>
<li><%= friend %></li>
<%_ }) %>
</ul>

The snippet above wrapped <%_ %> around the control-flow syntax to tell EJS to trim any whitespace before the template tags.

Use the whitespace slurping ending tag to remove whitespace after the closing tag.

<ul>
<% friends.forEach(function (friend) { _%>
<li><%= friend %></li>
<% }) _%>
</ul>

The snippet above wrapped <% _%> around the control-flow syntax to tell EJS to trim any whitespace after the template tag.

Use the comment tag to add EJS comments that are ignored during rendering and do not appear in the generated HTML.

<ul>
<%# Loop through the friends array %>
<% friends.forEach(function (friend) { _%>
<li><%= friend %></li>
<% }) _%>
</ul>

The snippet above wrapped the comment in <%# %>.

The following sections will guide you through the process of using EJS in your Express.js project.

First, install EJS in your Express project.

Terminal window
npm install ejs

Next, build a second web server to serve dynamically generated views.

Configure a web server to serve dynamic views

Section titled “Configure a web server to serve dynamic views”

Create an index-002.js file that sets up your app to handle client requests.

Terminal window
touch index-002.js

Open the module and configure the web server to serve the appropriate dynamically generated HTML view in response to requests.

index-002.js
// Add the Express and EJS modules
import express from "express";
import ejs from "ejs";
// Create a new Express application instance
const app = express();
// Specify the host and port to receive requests
const host = "localhost";
const port = 8000;
// Define the data object
const friendsData = {
bestFriend: "Sarah",
boyFriend: "Abraham",
dreamFriend: "Mary",
};
const replacementObject = { friends: friendsData };
// Use backticks to define a template string with embedded JavaScript
const templateString = `
<html>
<body>
<h1>List of Friends</h1>
<ul>
<%# Loop through the friends object %>
<% for (const eachFriend in friends) { -%>
<li><%= friends[eachFriend] %> is my <%= eachFriend %></li>
<% } -%>
</ul>
</body>
</html>
`;
// Compile and render the template string to HTML (the rendered view)
const htmlData = ejs.render(templateString, replacementObject);
// Use the dynamic view as a response to 'GET /' requests
app.get("/", (req, res) => res.send(htmlData));
// Run the server on the specified port and hostname:
app.listen(port, host, () => {
console.log(`Server running live at http://${host}:${port}!`);
});

Here are the main things the snippet above does:

  • Create an Express application instance to access Express APIs, such as routers and middleware, which simplifies HTTP request handling in Node.js.
  • Use the template string to generate the final HTML that the server sends to the client.
  • Create routes to receive and respond to clients’ requests.
  • Use Express’s app.listen() method to start the Node.js HTTP server on a specific port and execute a callback when the server begins listening for client requests.

After setting up the server, run it with Node.js.

Terminal window
node --watch index-002.js

Afterward, check your app running live at http://localhost:8000.

To keep things organized, put all your view templates in a views directory. This is a common practice.

Create a views directory at your project’s root to store all view templates.

Terminal window
mkdir views
Terminal window
touch views/index.ejs

Open the file and move the HTML template from index-002.js into it.

views/index.ejs
<html>
<body>
<h1>List of Friends</h1>
<ul>
<%# Loop through the friends object %>
<% for (const eachFriend in friends) { -%>
<li><%= friends[eachFriend] %> is my <%= eachFriend %></li>
<% } -%>
</ul>
</body>
</html>

Update the server to retrieve view templates from the views folder.

Section titled “Update the server to retrieve view templates from the views folder.”
index.js
import express from "express";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const app = express();
const host = "localhost";
const port = 8000;
const __dirname = dirname(fileURLToPath(import.meta.url));
// Specify the app's views directory
app.set("views", join(__dirname, "views"));
// Specify the app's view engine
app.set("view engine", "ejs");
// Define the data object
const friendsData = {
bestFriend: "Sarah",
boyFriend: "Abraham",
dreamFriend: "Mary",
};
const replacementObject = { friends: friendsData };
// Compile and render the index view template to HTML and use it as a response to 'GET /' requests
app.get("/", (req, res) => {
res.render("index", replacementObject);
});
// Run the server on the specified port and hostname:
app.listen(port, host, () => {
console.log(`Server running live at http://${host}:${port}`);
});

The snippet above uses Express’ render() method to generate HTML from the index.ejs template file.

How to create reusable EJS templates (Partials)

Section titled “How to create reusable EJS templates (Partials)”

EJS provides the include() method for including (nesting) one view template into another.

The include() method accepts two arguments. Here’s the syntax:

<%- include("path/to/partial", data) %>
  • "path/to/partial": (required) The path to the reusable template, relative to the current file (the parent template). For example, if the current file is at "./views/index.ejs" and the partial is at "./views/partials/footer.ejs", the include() path argument would be "partials/footer".
  • data: (optional) An object containing the properties to pass to the partial.

How to include a partial in a view template

Section titled “How to include a partial in a view template”

Consider the following server file:

index-003.js
import express from "express";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const app = express();
const host = "localhost";
const port = 8080;
const __dirname = dirname(fileURLToPath(import.meta.url));
app.set("views", join(__dirname, "views"));
app.set("view engine", "ejs");
const friendsData = {
bestFriend: "Sarah",
boyFriend: "Abraham",
dreamFriend: "Mary",
};
const replacementObject = { friends: friendsData };
app.get("/", (req, res) => {
res.render("index", replacementObject);
});
app.listen(port, host, () => {
console.log(`Server running live at http://${host}:${port}`);
});

The snippet above will render an index template when users send a GET request to the "/" path. Below is the index.ejs template file.

views/index.ejs
<html>
<body>
<h1>List of Friends</h1>
<ul>
<%# Loop through the friends object %>
<% for (const eachFriend in friends) { -%>
<li><%= friends[eachFriend] %> is my <%= eachFriend %></li>
<% } -%>
</ul>
</body>
</html>

If you need the <li> element to be reusable in multiple templates, extract it into a separate file and use the include() method to add it to any template as needed. Here’s how:

Create a partials directory in your views folder to store all reusable view templates.

Terminal window
mkdir views/partials

Create a partial view template for the <li> element.

Terminal window
touch views/partials/friendLi.ejs

Open the file and add the <li> element.

views/partials/friendLi.ejs
<li><%= friend %> is my <%= friendType %></li>

Open the index.ejs file and use the include() method to add the friendLi.ejs partial view template.

views/index.ejs
<html>
<body>
<h1>List of Friends</h1>
<ul>
<%# Loop through the friends object %>
<% for (const eachFriend in friends) { -%>
<%- include("partials/friendLi", { friend: friends[eachFriend], friendType: eachFriend }) %>
<% } -%>
</ul>
</body>
</html>

Using include() to add the <li> element makes it reusable in multiple templates, rather than restricting it to the index.ejs view template.

Terminal window
node --watch index-003.js

Then, check your app running live at http://localhost:8080.