Express Complete Tutorial Part 2: Render EJS Views

The browser should get an order list, not a dump of the data your Express route holds. I’m drawn to EJS’s split between JavaScript that controls a template and tags that print values.

Pass the route’s title and order data into an EJS view. Begin by connecting those values to the HTML your Express response sends.

What EJS changes in an Express response

An EJS template is an HTML file with tags that let JavaScript insert values or choose which markup to render. Express calls the view engine from a route, then sends the generated HTML to the browser.

Express chooses the template and supplies its data, so EJS can render that page with the route’s values. The Express template-engine guide and EJS documentation explain the view settings and template tags behind that step.

TagWhat it does
<% … %>Runs JavaScript without writing its value into the HTML.
<%= … %>Writes a value after HTML escaping it.
<%- … %>Writes unescaped output, which is useful for trusted template markup such as an include.

What you need before rendering a view

Start with a running Express app, then add a views directory beside the app file. If you still need the server setup, Part 1 of this Express series handles that step.

  • Node.js and npm installed on your machine.
  • An app file that creates an Express application.
  • A page at views/home.ejs and a partial at views/partials/navigation.ejs.

Render an EJS view from an Express route

The route passes named values to EJS, and two order rows make the locals, template, and included navigation visible in one response.

Step 1. Install Express and EJS

Run npm from the project directory to add both packages. The command leaves package versions unpinned.

npm install express ejs --no-package-lock

Step 2. Configure the view engine and route

Express needs an EJS view engine and a directory for its templates. This route passes a title, a user name, and order records as locals, which are the values available to the rendered view.

const express = require('express');
const path = require('node:path');

const app = express();

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

const orders = [
  { id: 104, status: 'Shipped' },
  { id: 105, status: 'Processing' },
];

app.get('/', (_req, res) => {
  res.render('home', {
    pageTitle: 'Orders',
    userName: 'Maya',
    orders,
  });
});

const port = 3187;

app.listen(port, () => {
  console.log(`Listening on http://localhost:${port}`);
});

The views path is built from the app file’s location, so starting Node from another working directory does not change where Express looks. With the view engine selected, res.render(‘home’, …) resolves home.ejs and sends the rendered HTML.

Step 3. Write the page template

The second argument to res.render() becomes the template’s locals. An escaped-output tag prints values as text, while a scriptlet runs the loop without adding JavaScript to the response.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title><%= pageTitle %></title>
  </head>
  <body>
    <header>
      <h1><%= pageTitle %></h1>
      <p>Signed in as <%= userName %></p>
    </header>
    <%- include('partials/navigation', { pageTitle }) %>
    <main>
      <h2>Open orders</h2>
      <ul>
        <% orders.forEach((order) => { %>
          <li>Order <%= order.id %>: <%= order.status %></li>
        <% }); %>
      </ul>
    </main>
  </body>
</html>

Each callback runs once for an order and emits its identifier and status through escaped-output tags in the surrounding HTML. I tested the tag with script input, and EJS returned encoded text instead of markup.

Step 4. Reuse markup with an EJS include

The home view calls EJS include() to insert its navigation file. The include returns markup, so the raw-output tag keeps its HTML intact, while values inside the partial still use escaped output.

<nav aria-label="Main navigation">
  <a href="/">Home</a>
  <span>Current view: <%= pageTitle %></span>
</nav>

The include path is relative to home.ejs, so partials/navigation points to views/partials/navigation.ejs. The call passes pageTitle explicitly, making the partial’s data requirement visible where it is included.

Step 5. Start the server

Start the server in a terminal beside app.js and leave it running while you request the page from another terminal.

node app.js
Express renders an EJS Orders view with included navigation and two order statuses
The route passes values into EJS, which sends the rendered page.

The served page shows the route locals in the heading and greeting, then the included navigation and both order rows. Express sends HTML to the browser, not the EJS source file.

Fix the view errors that stop rendering

When a page fails to render, check the file lookup and the names shared between the route and template before changing the view engine.

SymptomCheckRepair
Express cannot find the home view.The configured views directory and the file name.Keep home.ejs inside that directory, or point the views setting at the folder that contains it.
A value is missing in the page.Names such as pageTitle, userName, and orders in the route and template.Pass the matching key in the locals object given to res.render().
The navigation include cannot be found.The partial path relative to the template that calls include().Place navigation.ejs under views/partials for the path used above.
User text is treated as markup.Whether the template uses escaped or raw output.Use escaped output for dynamic values. Reserve raw output for trusted template markup, and do not pass an entire request query object as EJS locals.

A misspelled key stays missing. EJS exposes values from the locals object under the names the route passed, and its security guidance warns against handing request query data directly to rendering.

Keep route data separate from template output

Let the route choose and validate the values a page needs, then let EJS format those values as HTML. Passing the entire query object to res.render() gives request data more influence over rendering than this view needs.

curl --fail --silent --show-error http://127.0.0.1:3187/

The response should contain the Orders title, Maya greeting, included navigation, and both order statuses. Once this view works, continue to Part 3 on handling Express routes.

Frequently asked questions

EJS’s current include syntax also clears up the partial helper error that appears in older examples.

Why does EJS report that partial is not defined?

The view calls partial(), but that helper is not defined in this EJS template context. Use include() inside the template and check that the file path is relative to the template making the call.

Does Express look in the views directory by default?

Yes. Express uses a views directory rooted in the application by default. Set the views application setting when you want to use a different folder.

Pankaj Kumar
Pankaj Kumar

Pankaj Kumar is the founder and CEO of CodeForGeek, with more than 14 years in IT. He is an open-source enthusiast who enjoys sharing what he learns through CodeForGeek and YouTube, with a focus on Python, data analytics, machine learning, Angular, Node.js, and Kafka.

Articles: 335