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:
npm install ejsImport it
Pick the module system that fits your project.
// ESMimport ejs from 'ejs';// CommonJSconst 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: <b>hi</b> %><%- 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:
ejs ./template.ejs -m '?' -o ./output.htmlSee 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.
Header and footer
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 templatesClearing 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
renderFileis unavailable. There is no filesystem to read from, so useejs.render()(orejs.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 theincluderoption, 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 stringOptions
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:
npm install -g ejsRunning a template
ejs ./template.ejs -f data.json -o ./output.htmlThis 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
# From a JSON fileejs ./template.ejs -f data.json
# Inline as a URI-encoded JSON stringejs ./template.ejs -i '%7B%22name%22%3A%22world%22%7D'
# From stdinejs ./template.ejs < data.json
# As key=value pairs after the templateejs ./template.ejs name=worldPass 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:
ejs -p [ -c ] ./template_file.ejs -o ./output.htmlejs -n -l _ ./some_template.ejs -f ./data_file.jsonCommand-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=valuedata 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.