NodeJSEJS

EJS

EJS (Embedded JavaScript) is the most approachable template engine: a .ejs file is just HTML with JavaScript embedded in <% %> tags. There's no new language to learn — you write the markup you already know and drop in real JS expressions for the dynamic parts. This page covers the tag syntax, escaping, loops and conditionals, and how to structure pages with partials.

Setup

Bash
npm install ejs

app.js

JS
import express from 'express'
const app = express()

app.set('view engine', 'ejs')      // files end in .ejs
app.set('views', './views')         // template directory

app.get('/users/:id', async (req, res) => {
  const user = await db.users.find(req.params.id)
  if (!user) return res.status(404).render('404')
  res.render('users/show', { user })   // renders views/users/show.ejs
})
The EJS tags

Tag

Purpose

<%= value %>

Output, HTML-escaped (safe default)

<%- value %>

Output, raw/unescaped (dangerous with user data)

<% code %>

Run JS, no output (loops, ifs, assignments)

<%# comment %>

Comment, not rendered

<%- include(...) %>

Include a partial template

<%_ _%> / -%>

Whitespace-trimming variants

`<%=` escapes, `<%-` does not — the one distinction to memorize
The dash vs equals sign is the whole security story: `<%= %>` escapes HTML entities (safe for any value), `<%- %>` outputs raw HTML (only for trusted markup). Everything else is ordinary JavaScript between `<%` and `%>`. If you remember nothing else about EJS, remember which one escapes.
A complete template

views/users/show.ejs

HTML
<!DOCTYPE html>
<html>
<head><title><%= user.name %></title></head>
<body>
  <%- include('../partials/header') %>

  <h1>Welcome, <%= user.name %></h1>     <!-- escaped: safe -->
  <p>Email: <%= user.email %></p>

  <% if (user.isAdmin) { %>
    <a href="/admin">Admin panel</a>
  <% } else { %>
    <p>Standard account</p>
  <% } %>

  <h2>Recent orders</h2>
  <ul>
    <% user.orders.forEach((order) => { %>
      <li><%= order.title %> — $<%= order.total.toFixed(2) %></li>
    <% }) %>
  </ul>

  <% if (user.orders.length === 0) { %>
    <p>No orders yet.</p>
  <% } %>

  <%- include('../partials/footer') %>
</body>
</html>
Loops and conditionals are just JavaScript
Notice there's no special template DSL for control flow — `if/else` and `forEach` are plain JS inside `<% %>`. The opening `<% ... { %>` and closing `<% } %>` wrap the HTML you want repeated or conditionally shown. Because it's real JavaScript, anything you can do in JS (`.map`, `.filter`, ternaries, function calls like `.toFixed`) works directly in the template.
Passing data — and handling missing values

JS
res.render('dashboard', {
  title: 'Dashboard',
  user: req.user,
  notifications: notifications ?? [],   // pass a default, not undefined
})
Referencing an undefined variable throws — pass every variable the template uses
If a template references `<%= title %>` but you didn't include `title` in the render data, EJS throws `title is not defined` and the request errors out. Always pass *every* variable the template reads (use `?? []` / `?? ''` defaults for optional ones), or guard with `typeof title !== 'undefined'`. A common pattern is setting shared defaults via `app.locals` / `res.locals` so every view has them.
res.locals for data shared across views

JS
// Middleware: make these available to EVERY render automatically.
app.use((req, res, next) => {
  res.locals.currentUser = req.user
  res.locals.siteName = 'Acme'
  next()
})

// Now any template can use <%= siteName %> without it being passed each time.
`res.locals` injects per-request data into every view
Data on `res.locals` is merged into the render context for that request, so templates (and partials) can read `currentUser` or `siteName` without each handler re-passing them. Use `app.locals` for app-wide constants and `res.locals` for per-request values (the logged-in user, flash messages). This is the clean way to feed shared data to your header/footer partials.
Partials with parameters

Passing data into an include

HTML
<!-- views/partials/card.ejs -->
<div class="card">
  <h3><%= title %></h3>
  <p><%= body %></p>
</div>

<!-- using it, passing locals: -->
<%- include('partials/card', { title: 'Hello', body: 'World' }) %>
Use `<%-` for `include`, not `<%=`
`include()` returns HTML markup, so it must be output **raw** with `<%- include(...) %>`. Write `<%= include(...) %>` and EJS escapes the partial's HTML into visible `&lt;div&gt;` text on the page. This is the one legitimate, safe use of `<%-` — the partial's HTML is yours, not user input.
When EJS fits
  • You want to write HTML you already know, with minimal new syntax.

  • You like having full JavaScript available in templates.

  • You're generating emails, server-rendered pages, or admin views.

  • You prefer Pug if you want terser, indentation-based markup, or Handlebars if you want to restrict logic in templates.

Next
A terser, indentation-driven alternative: [Pug](/nodejs/pug).