Skip to content

EJS Templating with Express

EJS (Embedded JavaScript) is a template engine that looks almost exactly like plain HTML, with small embedded JavaScript tags for dynamic content. This makes it one of the easiest engines to pick up if you already know HTML and JavaScript.

EJS Syntax Basics

EJS uses <%= %> to output an escaped value, <%- %> to output raw, unescaped HTML, and <% %> for control flow (loops, conditionals) that produces no output itself.

Because EJS templates are otherwise plain HTML, existing static markup can often be converted directly into a template just by adding a few embedded tags.

<!-- views/profile.ejs -->
<!DOCTYPE html>
<html>
  <body>
    <h1>Hello, <%= name %>!</h1>
    <% if (isAdmin) { %>
      <p>You have admin access.</p>
    <% } %>
  </body>
</html>

<%= name %> outputs the escaped value of the name variable; the <% if %> block controls whether the admin paragraph renders at all.

EJS Tag Reference

<%= value %>   escaped output (safe for user data)
<%- html %>    unescaped output (raw HTML)
<% code %>     control flow, no output
<%# comment %> comment, not rendered
  • Always use <%= %> for any value that came from user input, to prevent XSS.
  • Only use <%- %> for HTML you generated and trust yourself, like a rendered partial.
  • Loops and conditionals use plain JavaScript syntax inside <% %> tags.
  • Install with npm install ejs, no separate configuration package is required.

EJS Cheatsheet

Common EJS patterns for lists, conditionals, and partials.

Task Syntax
Escaped output <%= user.name %>
Raw HTML output <%- markdownHtml %>
Conditional <% if (loggedIn) { %> ... <% } %>
Loop <% items.forEach(item => { %> ... <% }) %>
Include a partial <%- include('partials/header') %>

Looping Over Data

EJS control-flow tags accept any valid JavaScript, so rendering a list is just a forEach (or for/map) loop with an output tag inside it.

<ul>
  <% products.forEach(product => { %>
    <li><%= product.name %> - $<%= product.price %></li>
  <% }) %>
</ul>

Partials for Shared Layout

include() inserts another EJS file's rendered output at that point, useful for a shared header, footer, or navigation menu reused across many pages.

<!-- views/partials/header.ejs -->
<header><h1><%= siteName %></h1></header>

<!-- views/home.ejs -->
<%- include('partials/header') %>
<main>Welcome!</main>

Common Mistakes

  • Using <%- %> for user-generated content, opening an XSS vulnerability.
  • Forgetting that <% %> produces no output at all, useful for control flow but not for displaying values.
  • Not setting app.set('views', ...) correctly, causing include() paths to resolve incorrectly.
  • Writing complex business logic directly inside a template instead of preparing the data in the route handler first.

Key Takeaways

  • EJS templates look like plain HTML with embedded JavaScript output and control-flow tags.
  • <%= %> escapes output (safe for user data); <%- %> outputs raw HTML (only for trusted content).
  • Loops and conditionals inside <% %> use ordinary JavaScript syntax.
  • include() composes shared partials like headers and footers across multiple pages.

Pro Tip

Default to <%= %> everywhere and only reach for <%- %> when you specifically need to render pre-built, trusted HTML (like an included partial), this habit alone prevents most templating-related XSS bugs.