NexusCS

EJS

JavaScript
Embedded JavaScript templating - Simple, fast templating language for Node.js with plain JavaScript syntax.
templating
nodejs
express

Getting started

Introduction

EJS (Embedded JavaScript) is a templating engine that lets you generate HTML with plain JavaScript. Simple syntax, fast execution, and easy to debug.

23.8M weekly downloads on npm.

Installation

# Install via npm
npm install ejs
# Install globally for CLI
npm install -g ejs

Current stable version: 4.0.1

Quick Example

const ejs = require("ejs");

const template = "<h1>Hello, <%= name %>!</h1>";
const html = ejs.render(template, { name: "World" });
// => <h1>Hello, World!</h1>
// Render from file
ejs
  .renderFile("template.ejs", { user: "John" })
  .then((html) => console.log(html));

Template Tags

Core Syntax

Tag Description
<% Scriptlet - Control-flow, no output
<%= Escaped output - HTML-safe (use for user data)
<%- Unescaped output - Raw HTML (trusted only)
<%# Comment - No execution, no output
<%% Literal - Outputs literal <%

Whitespace Control

Tag Description
-%> Trim newline after tag
_%> Remove all whitespace after tag
<%_ Remove all whitespace before tag

Tag Examples

<!-- Scriptlet (no output) -->
<% const user = 'John'; %>
<% if (user) { %>
  <p>Welcome!</p>
<% } %>
<!-- Escaped output (safe) -->
<p>Hello, <%= username %></p>
<!-- Input: { username: '<script>' } -->
<!-- Output: Hello, &lt;script&gt; -->
<!-- Unescaped output (dangerous!) -->
<div><%- trustedHTML %></div>
<!-- Output: Raw HTML without escaping -->
<!-- Comments (not in output) -->
<%# This is a comment %>
<%# TODO: Add error handling %>

Rendering Methods

ejs.render()

const ejs = require("ejs");

// Basic rendering
const html = ejs.render("<h1><%= title %></h1>", { title: "My Page" });
// With options
const html = ejs.render(template, data, {
  cache: true,
  filename: "template.ejs",
  root: "./views",
});

ejs.renderFile()

// Async rendering from file
ejs
  .renderFile("template.ejs", { user: "John" })
  .then((html) => console.log(html))
  .catch((err) => console.error(err));
// With callback
ejs.renderFile("view.ejs", data, options, (err, html) => {
  if (err) throw err;
  console.log(html);
});

ejs.compile()

// Compile for reuse
const template = ejs.compile("<h1><%= title %></h1>", {
  filename: "title.ejs",
});

// Render multiple times (fast)
const html1 = template({ title: "Page 1" });
const html2 = template({ title: "Page 2" });
// Async compile
const template = ejs.compile(str, { async: true });
const html = await template(data);

Express.js Integration

Basic Setup

const express = require("express");
const app = express();

// Set EJS as view engine
app.set("view engine", "ejs");

// Set views directory (default: ./views)
app.set("views", "./views");

app.get("/", (req, res) => {
  res.render("index", { title: "Home" });
});

Views Directory Structure

project/
├── views/
│   ├── index.ejs
│   ├── about.ejs
│   └── partials/
│       ├── header.ejs
│       └── footer.ejs
├── app.js
└── package.json

Rendering Templates

// Render with data
app.get("/user/:id", (req, res) => {
  res.render("user", {
    user: { name: "John", id: req.params.id },
  });
});
// With layout
app.get("/page", (req, res) => {
  res.render("page", {
    title: "My Page",
    content: "Page content here",
  });
});

Global Options

// Set global data (available in all templates)
app.locals.siteName = "My Website";
app.locals.version = "1.0.0";

// Custom options
app.set("view options", {
  delimiter: "?",
  rmWhitespace: true,
});

Common Options

Compilation Options

Option Type Description
cache Boolean Cache compiled functions
filename String Required for includes/caching
root String Template root directory
views Array Directories for includes
context Object Function execution context
compileDebug Boolean Debug info (default: true)
client Boolean Compile for browser
delimiter String Custom delimiter (default: %)
strict Boolean Use strict mode
_with Boolean Use with() (default: true)
localsName String Locals object name (default: locals)

Rendering Options

Option Type Description
async Boolean Use async/await functions
rmWhitespace Boolean Remove safe whitespace
escape Function Custom escape function
outputFunctionName String Output function name

Options Examples

// Cache for production
const html = ejs.render(template, data, {
  cache: process.env.NODE_ENV === "production",
  filename: "template.ejs",
});
// Custom delimiter
const html = ejs.render(
  "<h1><? title ?></h1>",
  { title: "Custom" },
  { delimiter: "?" },
);
// Remove whitespace
const html = ejs.render(template, data, {
  rmWhitespace: true,
});

Includes & Partials

Basic Includes

<!-- Include without data -->
<%- include('partials/header') %>

<main>
  <h1>Page Content</h1>
</main>

<%- include('partials/footer') %>
<!-- Include with data -->
<%- include('user/profile', { user: currentUser }) %>

<!-- Include with merged data -->
<%- include('widget', { ...defaults, custom: true }) %>

File Paths

<!-- Relative to template directory -->
<%- include('header') %>
<%- include('partials/nav') %>

<!-- Absolute path (from root option) -->
<%- include('/layouts/main') %>

<!-- With .ejs extension -->
<%- include('header.ejs') %>

Nested Includes

<!-- layout.ejs -->
<!DOCTYPE html>
<html>
<%- include('partials/head') %>
<body>
  <%- include('partials/nav') %>
  <%- body %>
  <%- include('partials/footer') %>
</body>
</html>
<!-- partials/head.ejs -->
<head>
  <title><%= title %></title>
  <%- include('meta') %>
</head>

Control Flow

Conditionals

<!-- if/else -->
<% if (user) { %>
  <h1>Welcome, <%= user.name %></h1>
<% } else { %>
  <h1>Welcome, Guest</h1>
<% } %>
<!-- else if -->
<% if (user.role === 'admin') { %>
  <a href="/admin">Admin Panel</a>
<% } else if (user.role === 'moderator') { %>
  <a href="/moderate">Moderate</a>
<% } else { %>
  <a href="/profile">Profile</a>
<% } %>
<!-- Ternary operator -->
<p class="<%= user ? 'logged-in' : 'guest' %>">
  <%= user ? user.name : 'Guest' %>
</p>

Loops

<!-- for loop -->
<ul>
  <% for (let i = 0; i < items.length; i++) { %>
    <li><%= items[i] %></li>
  <% } %>
</ul>
<!-- forEach -->
<ul>
  <% items.forEach(item => { %>
    <li><%= item.name %> - $<%= item.price %></li>
  <% }); %>
</ul>
<!-- for...of -->
<% for (const user of users) { %>
  <div class="user">
    <h3><%= user.name %></h3>
    <p><%= user.email %></p>
  </div>
<% } %>
<!-- Array methods -->
<% const activeUsers = users.filter(u => u.active); %>
<% activeUsers.map(u => u.name).forEach(name => { %>
  <li><%= name %></li>
<% }); %>

Data Access

Accessing Variables

<!-- Direct access -->
<h1><%= title %></h1>
<p><%= description %></p>
<!-- Object properties -->
<h2><%= user.name %></h2>
<p><%= user.profile.bio %></p>
<!-- Array elements -->
<%= items[0] %>
<%= users[index].name %>

Optional Chaining

<!-- Safe property access -->
<%= user?.name %>
<%= post?.author?.name %>
<%= config?.api?.endpoint %>
<!-- With default values -->
<%= user?.name || 'Anonymous' %>
<%= settings?.theme || 'light' %>

Functions & Methods

<!-- Call functions -->
<%= formatDate(date) %>
<%= calculateTotal(items) %>
<!-- Array methods -->
<%= users.length %>
<%= items.join(', ') %>
<%= names.slice(0, 3) %>
<!-- String methods -->
<%= title.toUpperCase() %>
<%= text.substring(0, 100) %>

Complete Examples

Blog Post Template

<!DOCTYPE html>
<html lang="en">
<head>
  <title><%= post.title %> - <%= siteName %></title>
</head>
<body>
  <%- include('partials/header') %>

  <article>
    <h1><%= post.title %></h1>
    <p class="meta">
      By <%= post.author.name %> on
      <%= new Date(post.date).toLocaleDateString() %>
    </p>

    <div class="content">
      <%- post.content %>
    </div>

    <% if (post.tags && post.tags.length) { %>
      <div class="tags">
        <% post.tags.forEach(tag => { %>
          <span class="tag"><%= tag %></span>
        <% }); %>
      </div>
    <% } %>
  </article>

  <section class="comments">
    <h2>Comments (<%= comments.length %>)</h2>
    <% comments.forEach(comment => { %>
      <%- include('partials/comment', { comment }) %>
    <% }); %>
  </section>

  <%- include('partials/footer') %>
</body>
</html>

User List with Filters

<div class="user-list">
  <h2>Users (<%= users.length %>)</h2>

  <% const activeUsers = users.filter(u => u.active); %>
  <% const inactiveUsers = users.filter(u => !u.active); %>

  <% if (activeUsers.length > 0) { %>
    <section>
      <h3>Active Users (<%= activeUsers.length %>)</h3>
      <ul>
        <% activeUsers.forEach(user => { %>
          <li class="user-item">
            <strong><%= user.name %></strong>
            <span><%= user.email %></span>
            <% if (user.role === 'admin') { %>
              <span class="badge">Admin</span>
            <% } %>
          </li>
        <% }); %>
      </ul>
    </section>
  <% } %>

  <% if (inactiveUsers.length > 0) { %>
    <section>
      <h3>Inactive Users (<%= inactiveUsers.length %>)</h3>
      <ul>
        <% inactiveUsers.forEach(user => { %>
          <li class="user-item inactive">
            <strong><%= user.name %></strong>
          </li>
        <% }); %>
      </ul>
    </section>
  <% } %>

  <% if (users.length === 0) { %>
    <p class="empty">No users found.</p>
  <% } %>
</div>

Reusable Card Component

<!-- components/card.ejs -->
<div class="card <%= type || 'default' %>">
  <% if (image) { %>
    <img src="<%= image %>" alt="<%= title %>">
  <% } %>

  <div class="card-body">
    <% if (title) { %>
      <h3 class="card-title"><%= title %></h3>
    <% } %>

    <% if (description) { %>
      <p class="card-description"><%= description %></p>
    <% } %>

    <% if (content) { %>
      <div class="card-content">
        <%- content %>
      </div>
    <% } %>

    <% if (link) { %>
      <a href="<%= link.url %>" class="card-link">
        <%= link.text || 'Read more' %>
      </a>
    <% } %>
  </div>
</div>
<!-- Usage -->
<%- include('components/card', {
  type: 'featured',
  image: '/img/post.jpg',
  title: 'Article Title',
  description: 'Brief description here',
  link: { url: '/posts/1', text: 'Read article' }
}) %>

CLI Usage

Basic Commands

# Render template to stdout
ejs template.ejs

# Output to file
ejs template.ejs -o output.html

# Pass data from JSON file
ejs template.ejs -f data.json

# Pass data inline
ejs template.ejs -i '{"name":"John"}'

CLI Options

Option Description
-o, --output-file FILE Write to FILE
-f, --data-file FILE JSON data file
-i, --data-input STRING Inline JSON data
-m, --delimiter CHARACTER Custom delimiter
-p, --open-delimiter STRING Custom opening delimiter
-c, --close-delimiter STRING Custom closing delimiter
--rm-whitespace Remove whitespace
--strict Enable strict mode

CLI Examples

# Custom delimiter
ejs template.ejs -m '?' -o output.html

# Remove whitespace
ejs template.ejs --rm-whitespace -o output.html

# Multiple options
ejs template.ejs -f data.json --rm-whitespace -o dist/index.html

Security

Critical Security Warning

NEVER pass user input directly to ejs.render() or allow users to control template content!

EJS templates execute arbitrary JavaScript code. User-controlled templates = Remote Code Execution (RCE).

// DANGEROUS - Never do this!
const template = req.body.template; // User input
ejs.render(template, data); // RCE vulnerability!
// SAFE - Use predefined templates
const template = fs.readFileSync("template.ejs", "utf8");
ejs.render(template, { userInput: req.body.text });

Safe Output Practices

<!-- SAFE: Always escape user data -->
<p>Username: <%= userInput %></p>
<!-- Output: &lt;script&gt;alert(1)&lt;/script&gt; -->

<!-- DANGEROUS: Unescaped user data -->
<div><%- userInput %></div>
<!-- Output: <script>alert(1)</script> - XSS! -->
<!-- SAFE: Escape in attributes -->
<input value="<%= userInput %>">

<!-- SAFE: Escape in URLs -->
<a href="/user/<%= encodeURIComponent(userId) %>">Profile</a>

Security Best Practices

// 1. Never render user-provided templates
const ALLOWED_TEMPLATES = ["home", "user", "post"];
if (!ALLOWED_TEMPLATES.includes(template)) {
  throw new Error("Invalid template");
}
// 2. Sanitize data before passing to template
const sanitized = {
  name: validator.escape(user.name),
  bio: sanitizer.sanitize(user.bio),
};
ejs.render(template, sanitized);
<!-- 3. Use <%= for all user data -->
<h1><%= user.name %></h1>
<p><%= user.bio %></p>

<!-- 4. Only use <%- for trusted/sanitized HTML -->
<div><%- sanitizedHTML %></div>

Common Patterns

Layout Wrapper

<!-- layouts/main.ejs -->
<!DOCTYPE html>
<html>
<head>
  <title><%= title || 'Default Title' %></title>
  <%- head || '' %>
</head>
<body>
  <%- include('../partials/nav') %>
  <main>
    <%- body %>
  </main>
  <%- include('../partials/footer') %>
</body>
</html>
// Usage in Express
app.get("/", (req, res) => {
  res.render("layouts/main", {
    title: "Home",
    body: "<h1>Welcome</h1>",
  });
});

Conditional Classes

<!-- Method 1: Ternary -->
<div class="card <%= featured ? 'featured' : '' %>">

<!-- Method 2: Array join -->
<div class="<%=
  ['card', active && 'active', error && 'error']
    .filter(Boolean)
    .join(' ')
%>">

<!-- Method 3: Template literal -->
<div class="card <%= `${type} ${state}` %>">

Data Transformation

<%
  // Transform data in scriptlet
  const sortedUsers = users.sort((a, b) =>
    a.name.localeCompare(b.name)
  );

  const groupedByRole = users.reduce((acc, user) => {
    acc[user.role] = acc[user.role] || [];
    acc[user.role].push(user);
    return acc;
  }, {});
%>

<% Object.entries(groupedByRole).forEach(([role, users]) => { %>
  <h3><%= role %> (<%= users.length %>)</h3>
  <ul>
    <% users.forEach(user => { %>
      <li><%= user.name %></li>
    <% }); %>
  </ul>
<% }); %>

Helper Functions

<%
  // Define helpers in template
  function formatCurrency(amount) {
    return `$${amount.toFixed(2)}`;
  }

  function truncate(text, length = 100) {
    return text.length > length
      ? text.substring(0, length) + '...'
      : text;
  }
%>

<p><%= formatCurrency(price) %></p>
<p><%= truncate(description, 50) %></p>
// Or pass helpers as data
const helpers = {
  formatDate: (date) => date.toLocaleDateString(),
  formatCurrency: (n) => `$${n.toFixed(2)}`,
};

ejs.render(template, { ...data, ...helpers });

Gotchas

Filename Required for Includes

Includes require the filename option to resolve paths:

// Error: Cannot find include
ejs.render('<% include("header") %>', data);

// Fixed: Provide filename
ejs.render('<% include("header") %>', data, {
  filename: __dirname + "/views/index.ejs",
});

Cache Requires Filename

Caching requires filename to generate cache key:

// Cache won't work
ejs.render(template, data, { cache: true });

// Fixed: Provide filename
ejs.render(template, data, {
  cache: true,
  filename: "template.ejs",
});

Async/Await Support

Use async option for async operations:

// Won't work - async not enabled
const template = "<%= await getData() %>";
ejs.render(template, data); // Error

// Fixed: Enable async
ejs.render(template, data, { async: true }).then((html) => console.log(html));
<!-- In template with async option -->
<% const users = await fetchUsers(); %>
<% users.forEach(user => { %>
  <li><%= user.name %></li>
<% }); %>

Whitespace in Output

EJS preserves whitespace by default:

<!-- Produces extra whitespace -->
<ul>
  <% items.forEach(item => { %>
    <li><%= item %></li>
  <% }); %>
</ul>
<!-- Output has blank lines -->
<!-- Solution 1: Use trim tags -->
<ul>
  <% items.forEach(item => { -%>
    <li><%= item %></li>
  <%- }); -%>
</ul>

<!-- Solution 2: Use rmWhitespace option -->
ejs.render(template, data, { rmWhitespace: true });

Variable Scope

Variables declared in scriptlets have local scope:

<% if (condition) { %>
  <% const x = 10; %>
<% } %>
<%= x %> <!-- Error: x is not defined -->

<!-- Fixed: Declare outside block -->
<% let x; %>
<% if (condition) { %>
  <% x = 10; %>
<% } %>
<%= x %> <!-- Works -->

Also see