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, <script> -->
<!-- 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: <script>alert(1)</script> -->
<!-- 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
- EJS Official Documentation - Complete reference
- EJS on GitHub - Source code and issues
- EJS on npm - Package registry (23.8M weekly downloads)
- Express.js Template Engines - Integration guide
- Awesome EJS - Community resources