Skip to content

Documentation

EJS is a simple templating language that lets you generate HTML markup with plain JavaScript. It runs on the server (Node.js) and in the browser, ships with a CLI, and has zero dependencies.

Getting Started

Install

Add EJS to your project with npm:

Terminal window
npm install ejs

Import it

Pick the module system that fits your project.

// ESM
import ejs from 'ejs';
// CommonJS
const ejs = require('ejs');

Bundler and runtime compatibility

As of v6, the published package is verified against the following bundlers and alternate runtimes on every release:

Tool Supported
Rollup ✓
Rolldown ✓
tsdown ✓
esbuild ✓
Webpack ✓
Vite ✓
Browserify ✓
Bun ✓
Deno ✓

For Browserify, pass --node so it picks the main entry rather than the prebuilt UMD bundle.

Browser-targeted bundlers that support conditional exports get the prebuilt browser bundle through the browser export condition. That bundle compiles and renders template strings; loading templates from the filesystem with renderFile needs the Node entry point.

Render a string

Pass EJS a template string and some data. You get back HTML.

import ejs from 'ejs';
const people = ['geddy', 'neil', 'alex'];
const html = ejs.render('<%= people.join(", "); %>', { people });
// => "geddy, neil, alex"

Render a file

renderFile reads a template from disk. The third argument is an options object; the callback receives the rendered string.

import ejs from 'ejs';
ejs.renderFile('./template.ejs', { people }, (err, html) => {
if (err) throw err;
console.log(html);
});

You can also await it when you omit the callback:

const html = await ejs.renderFile('./template.ejs', { people });

Use it with Express

EJS complies with the Express view system, so it works out of the box: just set the view engine.

import express from 'express';
const app = express();
app.set('view engine', 'ejs');
app.get('/', (req, res) => {
res.render('index', { title: 'Home', people });
});

As of v7, EJS doesn’t read its options from Express’s render data, app.locals, or app.set('view options'). To set EJS options for an Express app, register a render function. Pass along Express’s views and view cache settings too, since a custom render function replaces the default handling of them:

const ejsOptions = {
delimiter: '?',
views: [].concat(app.get('views')),
cache: app.enabled('view cache'),
};
app.engine('ejs', (path, data, cb) => {
ejs.renderFile(path, data, ejsOptions, cb);
});

Tags

EJS templates are HTML with JavaScript embedded inside delimiter tags. By default the tags open with <% and close with %>. Each variant controls whether code runs, whether its result is printed, and how it is escaped.

Tag reference

Tag Name What it does
<% Scriptlet Runs control-flow JavaScript; produces no output.
<%_ Whitespace-slurping scriptlet Like <%, but strips all whitespace before it.
<%= Escaped output Prints the value into the template, HTML-escaped.
<%- Unescaped output Prints the raw value into the template (no escaping).
<%# Comment Does nothing and prints nothing.
<%% Literal Outputs a literal <%.
%> Closing tag Plain close.
-%> Newline-trimming close Trims the newline immediately after the tag.
_%> Whitespace-slurping close Strips all whitespace after the tag.

Scriptlets and output

Use a scriptlet (<%) for logic and an output tag (<%=) to print a value:

<% if (user) { %>
<h2><%= user.name %></h2>
<% } %>
<ul>
<% items.forEach(item => { %>
<li><%= item %></li>
<% }); %>
</ul>

Escaped vs. unescaped output

<%= escapes HTML so user-supplied values can’t inject markup. Use <%- only when you intend to emit raw HTML (for example, the result of an include):

<%# value is "<b>hi</b>" %>
<%= value %> <%# renders: &lt;b&gt;hi&lt;/b&gt; %>
<%- value %> <%# renders: <b>hi</b> %>

Escaping and output context

<%= escapes for HTML: specifically, for HTML content and quoted attribute values. That covers most templates, because most templates generate HTML.

EJS can generate any kind of text, though, and it has no idea where your output ends up. If you’re generating something else (SQL, shell scripts, plain-text email, config files), swap in escaping that fits:

ejs.render(template, data, { escape: myEscapeFunction });

To escape just one value, use the raw tag: <%- myEscapeFunction(name) %>. To set it for a whole Express app, see Use it with Express.

One spot to watch: inside a <script> tag, browsers don’t decode HTML entities, so <%= doesn’t do what you want there. Put the value in an attribute and JSON.parse it from your script instead.

Comments

<%# ... %> lets you annotate templates. The contents are never executed and never appear in the output.

<%# This block explains the markup below — it won't render %>
<p>Visible to the reader.</p>

Controlling whitespace

The slurping tags help you keep generated HTML tidy. -%> removes the single newline that follows a tag, while <%_ and _%> strip all surrounding whitespace:

<ul>
<% items.forEach(item => { -%>
<li><%= item %></li>
<% }); -%>
</ul>

For broader cleanup across the whole template, see the rmWhitespace option.

Includes

Includes let you pull one template into another: headers, footers, list items, anything reusable. Paths are resolved relative to the template that calls include().

Basic usage

Call include() inside an unescaped output tag (<%-) so the partial’s HTML isn’t double-escaped:

<ul>
<% users.forEach(user => { %>
<%- include('user/show', { user: user }); %>
<% }); %>
</ul>

user/show.ejs receives the data you pass as the second argument:

<%# user/show.ejs %>
<li><%= user.name %></li>

The raw-output tag

include() returns a string of already-rendered HTML. Print it with the escaping tag <%= and every <, >, and & gets escaped, so the markup shows up as literal text. The raw-output tag <%- emits it as-is.

Path resolution and filename

Relative include paths need to know where the calling template lives, so EJS requires the filename option to be set. When you use ejs.renderFile() or Express, filename is set automatically. If you call ejs.render() on a raw string and use includes, set it yourself:

const html = ejs.render(template, data, {
filename: '/path/to/template.ejs',
});

To resolve includes from a set of base directories (or with absolute paths like /partials/header), use the views and root options.

Preprocessor include (legacy)

Older EJS supported a literal <% include user/show %> form. It is deprecated and has no caching benefits. Prefer the include() function shown above.

Custom Delimiters

The default delimiters are <% and %>. You can change the inner character, the opening character, and the closing character, either per render or globally.

Per-template

Pass delimiter (the inner character, default %) in the options object:

import ejs from 'ejs';
const people = ['geddy', 'neil', 'alex'];
const html = ejs.render('<?= people.join(", "); ?>', { people }, {
delimiter: '?',
});
// uses <? ... ?> instead of <% ... %>

You can also change the outer characters with openDelimiter and closeDelimiter:

const html = ejs.render('[?= people.join(", "); ?]', { people }, {
delimiter: '?',
openDelimiter: '[',
closeDelimiter: ']',
});

Globally

Set the defaults on the ejs object once, and every subsequent render uses them:

import ejs from 'ejs';
ejs.delimiter = '?';
ejs.openDelimiter = '[';
ejs.closeDelimiter = ']';

On the command line

The CLI exposes the same controls via -m, -p, and -c:

Terminal window
ejs ./template.ejs -m '?' -o ./output.html

See the Options reference for the full list of delimiter-related settings.

Layouts

EJS does not have a dedicated layout or block-inheritance system. Instead, you compose pages from partials using includes, which is flexible enough to cover the common header/content/footer pattern.

Wrap each page’s content between a shared header and footer:

<%# page.ejs %>
<%- include('header'); -%>
<h1><%= title %></h1>
<p>This is the page body.</p>
<%- include('footer'); -%>
<%# header.ejs %>
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<%# footer.ejs %>
</body>
</html>

Because the header and footer share the same data scope as the page, title is available in all three templates.

Passing data into partials

Partials can take their own arguments. Pass them as the second argument to include():

<%- include('nav', { current: 'home' }); -%>

A note on trimming

Use the newline-trimming close tag -%> after an include() (as above) to keep your generated HTML from accumulating blank lines. See Tags for the full set of whitespace controls.

Caching

EJS compiles each template into a JavaScript function. Caching stores that compiled function so repeat renders of the same template skip the compile step.

Enabling the cache

Set the cache option to true. Because cached functions are keyed by file path, you must also provide a filename:

import ejs from 'ejs';
const html = ejs.render(template, data, {
cache: true,
filename: '/path/to/template.ejs',
});

When you render through ejs.renderFile() or Express, filename is set for you: just pass cache: true.

Swapping in a custom cache

By default EJS caches into a plain object that grows without bound. For long-running servers, assign your own cache (anything with set, get, and reset methods) to ejs.cache. An LRU cache is a common choice:

import ejs from 'ejs';
import { LRUCache } from 'lru-cache';
ejs.cache = new LRUCache({ max: 100 }); // keep the 100 most-recent templates

Clearing the cache

Call ejs.clearCache() to empty the cache, useful in development when templates change on disk:

ejs.clearCache();

Custom File Loader

The default file loader is fs.readFileSync. To customize how template files are read off disk, assign your own function to ejs.fileLoader:

import ejs from 'ejs';
import fs from 'fs';
const myFileLoader = function (filePath) {
return 'myFileLoader: ' + fs.readFileSync(filePath);
};
ejs.fileLoader = myFileLoader;

With this you can preprocess a template before it is read, for example to strip a custom header or pull templates from somewhere other than the filesystem.

Client-Side Support

EJS runs in the browser as well as on the server. Because browsers have no filesystem, a few file-oriented features behave differently. See the caveats below.

Add the script

Download ejs.js or ejs.min.js from the latest release and drop it in a script tag. EJS attaches itself to the global ejs object:

<script src="ejs.min.js"></script>
<script>
const people = ['geddy', 'neil', 'alex'];
const html = ejs.render('<%= people.join(", "); %>', { people });
document.body.innerHTML = html;
</script>

Alternatively, compile it yourself: clone the repository and run jake build (or $(npm bin)/jake build if jake is not installed globally).

Caveats in the browser

  • renderFile is unavailable. There is no filesystem to read from, so use ejs.render() (or ejs.compile()) with a template string instead.
  • File-based includes don’t work by default. Relative include() paths resolve against the filesystem. In the browser, supply your partials through the includer option, preload them as strings, or pass an include callback as the third argument to a compiled function:
const str = "Hello <%= include('file', {person: 'John'}); %>";
const fn = ejs.compile(str);
fn(data, null, function (path, d) { // include callback
// path -> 'file'
// d -> {person: 'John'}
// Return the contents of the file as a string
}); // returns the rendered string

Options

All EJS rendering functions (ejs.render(), ejs.renderFile(), and ejs.compile()) accept an optional options object as their final argument.

Common options

Option Default Description
cache false Compiled functions are cached; requires filename. See Caching.
filename — The template’s path. Used as the cache key and to resolve relative includes.
root — Project root for includes with an absolute path (e.g. /file.ejs). May be an array.
views — Array of paths searched when resolving relative includes.
context null Function execution context (this) inside the template.
compileDebug true When false, no debug instrumentation is compiled.
escape HTML escape The escaping function applied to <%= output.

Delimiter options

Option Default Description
delimiter % The inner delimiter character.
openDelimiter < The opening delimiter character.
closeDelimiter > The closing delimiter character.

See Custom Delimiters for examples.

Output & scope options

Option Default Description
strict false Generate the function in strict mode (disables with).
_with true Whether to use with(){} for the data scope. Disabling implies strict.
localsName locals Name of the object holding locals when with is disabled.
destructuredLocals [] Locals always destructured from the data object (available even in strict mode).
outputFunctionName — If set (e.g. 'echo'), exposes a print function for use inside scriptlet tags.
rmWhitespace false Remove all safe-to-remove whitespace, including leading/trailing. Also enables a safer version of -%> line slurping for all scriptlet tags: it does not strip newlines of tags in the middle of a line.
async false Use an async function for rendering, enabling await inside templates. Depends on async/await support in the JS runtime.

Advanced

Option Default Description
debug false Output the generated function body for inspection.
includer — Custom function to resolve and load includes.
unsafePrototypeLocals false Exposes the prototype chain of the locals object inside templates. Enabling it disables the v6 prototype-pollution mitigation.

CLI Usage

EJS ships with a command-line tool. Feed it a template file and some data, specify an output file, and it renders the result.

Installing the CLI

Install globally to get the ejs command on your PATH:

Terminal window
npm install -g ejs

Running a template

Terminal window
ejs ./template.ejs -f data.json -o ./output.html

This renders template.ejs using the data in data.json and writes the result to output.html. With no -o, output is written to stdout.

Providing data

Terminal window
# From a JSON file
ejs ./template.ejs -f data.json
# Inline as a URI-encoded JSON string
ejs ./template.ejs -i '%7B%22name%22%3A%22world%22%7D'
# From stdin
ejs ./template.ejs < data.json
# As key=value pairs after the template
ejs ./template.ejs name=world

Pass JSON data one way: stdin, -f, or -i. Using -f and -i together is an error. Stdin is only read when there’s no -f, -i, or key=value data. key=value pairs override values from -f or -i, and are always strings.

Some further examples, combining flags:

Terminal window
ejs -p [ -c ] ./template_file.ejs -o ./output.html
ejs -n -l _ ./some_template.ejs -f ./data_file.json

Command-line flags

Flag Description
-o, --output-file FILE Write output to FILE (default: stdout).
-f, --data-file FILE Load template data from a JSON FILE.
-i, --data-input STRING Must be JSON-formatted and URI-encoded. Use parsed input from STRING as data for rendering.
-m, --delimiter CHAR Inner delimiter character (default %).
-p, --open-delimiter CHAR Opening delimiter character (default <).
-c, --close-delimiter CHAR Closing delimiter character (default >).
-s, --strict Compile in strict mode.
-n, --no-with Don’t use with(); reference data via locals.
-l, --locals-name NAME Name of the locals object when --no-with is set.
-w, --rm-whitespace Remove safe-to-remove whitespace.
-d, --debug Output the generated function body.
-h, --help Show usage.
-V, -v, --version Print the EJS version.

These flags mirror the render options of the JavaScript API.

How arguments work

As of v7:

  • The template file comes first. Any key=value data comes after it.
  • Unknown options are an error.
  • Short options can be grouped (-sdw) and take attached values (-m$).
  • A value that starts with - needs =: -o=-out.html.
  • -- ends options, for example when a template’s name starts with -.
  • Exit status is 0 on success, 2 for a usage error, and 1 for a runtime error.

License

EJS is licensed under the Apache License, version 2.0. Information can be found at apache.org/licenses.