CLI tool
mde/ejs avatar
mde/ejs

mde/ejs: Embedded JavaScript Templates, and the Injection Risk You Own

Embedded JavaScript templates -- http://ejs.co

8,129 stars846 forksJavaScriptApache-2.0

At a glance

What is it?
EJS compiles templates into JavaScript functions and runs them. That design is why it is fast and flexible, and also why the README tells you not to pass user input straight into render.
Who is it for?
Adopt EJS when your templates are authored by your own team and the data is validated before it reaches render, which is the split the README draws. Do not adopt it as a sandbox for templates written by end users, because the project states plainly that it is effectively a JavaScript runtime and that unfettered access to render is inherently un-secure.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 51 days ago.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What EJS is for, and who ends up using it

EJS renders HTML from templates that contain ordinary JavaScript. A template is mostly markup, with scriptlet tags for control flow and output tags for values. The README's own example is three lines: an if block wrapping an h2 that prints user.name. That is the whole idea. There is no separate template language to learn, no expression grammar to map onto JavaScript, and no compile step you have to configure beyond calling the API.

The audience is narrow and specific. It is Node developers who already have Express in the stack, because EJS complies with the Express view system, and who want server-rendered HTML without adopting a component model. It also suits anyone who needs to render the same template in the browser, since the package ships a UMD bundle through the browser field and the README lists client-side support as a feature. If your team writes JavaScript all day, the template syntax costs nothing to learn.

The people who should look elsewhere are those who want a template language that cannot execute arbitrary code. EJS is not that, and the README is unusually direct about it: the entire job of the project is to execute JavaScript.

How a template becomes a function

EJS is a compiler, not an interpreter. When you call ejs.compile(str, options) you get back a function; calling that function with a data object returns a rendered string. Internally the template text is parsed into a function body that concatenates literal HTML chunks with the values your scriptlets produce, which is why the README can offer a debug option that outputs the generated function body and a compileDebug option that turns instrumentation off.

Three tags do the output work. <%= escapes, <%- does not, and <% runs control flow without emitting anything. Escaping defaults to XML, and the escape option lets you swap in a different function. Trailing dashes control whitespace: -%> trims the newline after a tag, and the <%_ _%> form slurps all whitespace around a scriptlet. Custom delimiters are available through openDelimiter, closeDelimiter and delimiter, so you can move off <% %> if your templates collide with another syntax.

Locals resolution is the part worth reading twice. By default EJS wraps the generated body in a with() {} construct so bare identifiers in the template resolve against your data object. Setting _with to false stores locals in an object instead, and strict mode forces that off. The localsName option renames that object from the default locals. This is the mechanism behind a v6 change: unsafePrototypeLocals defaults to false, which means top-level identifiers no longer resolve through the prototype chain of the locals object. If you pass a class instance or an Object.create result and rely on inherited properties, you now need to opt in, and the README states that enabling the option disables the v6 prototype-pollution mitigation.

Installing EJS and rendering your first file

Installation is a single npm command, and the package supports both module systems. The README shows the import and the require side by side, and notes that the CommonJS path supports Node versions at least back to v0.12 while the ES module path needs a Node that supports ESM. The CLI, exposed as the ejs binary, requires Node v8 or newer.

bash
npm install ejs

After that, pick the module form that matches your project. Both of these are quoted from the README.

javascript
import ejs from 'ejs';
// Or
const ejs = require('ejs');

The README's basic usage section gives three entry points. ejs.compile returns a reusable function, ejs.render renders a string directly, and ejs.renderFile reads from disk and hands the result to a callback.

javascript
const template = ejs.compile(str, options);
template(data);
// => Rendered HTML string

ejs.render(str, data, options);
// => Rendered HTML string

ejs.renderFile(filename, data, options, function(err, str){
    // str => Rendered HTML string
});

A template that uses the documented tags looks like this. The escaped tag prints the name, and the scriptlet around it emits nothing.

ejs
<% if (user) { %>
  <h2><%= user.name %></h2>
<% } %>

If you render files rather than strings, set filename so that cache has a key and includes can resolve, and set root when an include uses an absolute path such as /file.ejs. The README notes that root can be an array to try several directories, and that views takes an array of paths for includes resolved with relative paths.

The injection warning is the design, not a bug

The security section of the README opens by telling security professionals to read SECURITY.md before reporting anything, and then quotes the project's position: EJS is effectively a JavaScript runtime, its entire job is to execute JavaScript, and if you run the render method without checking inputs yourself, you are responsible for the results. It goes further and says it will not accept reports built on a route that passes req.query straight into res.render.

That is a defensible line, and it is also a real constraint on how you can use the library. Any template that reaches render with attacker-controlled content is server-side code execution, because the template is compiled into a function and run. The mitigation is architectural: keep templates in your repository, keep them out of your database and your upload path, and validate the data object before it gets there. The README's own phrasing is that you should never give end-users unfettered access to the render method.

The same reasoning applies to the options that relax behavior. unsafePrototypeLocals is off by default for a reason, and the README says turning it on disables the v6 prototype-pollution mitigation. If you enable it to make class instances work as locals, you are trading a safety default for convenience, and you should know which of your templates depend on inherited properties before you do.

Where EJS stops being the right tool

Two cases stand out. The first is user-authored templates. If your product lets customers write their own layouts, EJS is the wrong engine, because the template language is JavaScript and the render path executes it. You want a logic-less or explicitly sandboxed template language for that job, not one whose README describes it as a runtime.

The second is streaming. The public API in the README is compile, render and renderFile, and renderFile is callback-based. The async option exists and makes EJS use an async function for rendering, which depends on async/await support in the runtime, but the documented surface is still a function that returns a finished string rather than a stream you can pipe. If your pages are large and you care about time to first byte, that shape matters.

A third, smaller case: the README explicitly advises against the ejs.render(dataAndOptions) shortcut, where you pass data and options in one object. The reason given is that your code could break if a future option shares a name with one of your data properties. That is a maintenance hazard the project has already flagged, so treat it as unsupported even though it works.

EJS against JSX and other template approaches

The comparison people actually search for is EJS against JSX. The difference is where the JavaScript lives. In EJS the file is HTML with JavaScript embedded in tags, and the output is a string produced at render time. In JSX the file is JavaScript with markup embedded in it, and the output is a component tree that a framework reconciles against a virtual DOM. EJS has no component model, no reconciliation and no client-side state; it produces a string and stops. JSX gives you composition and updates but pulls in a build step and a runtime.

Against a logic-less engine the trade is the inverse. A logic-less template language restricts what a template can express, which makes templates safe to accept from less trusted authors and easier to reason about. EJS gives you the full language, which is why it is pleasant to write and why the security section exists at all. Pick based on who writes the templates, not on syntax preference.

There is also an internal comparison worth noting for anyone upgrading. Before v6 the published package emitted module.exports = ejs from inside the ESM source as a dual-mode shim, and the README says modern ESM-aware bundlers and Bun treated that as malformed ESM. In v6 the shim moved into the lib/cjs compile step, and the README states the published CJS surface is unchanged. The one bundler caveat it gives is Browserify: pass --node so it picks the main entry instead of the prebuilt UMD bundle named by the browser field.

Maintenance, upgrades and the Apache-2.0 licence

The repository is not archived, and the last push was on 2026-08-10. Releases are recent: v6.0.1 on 2026-05-26, v5.0.2 on 2026-04-11 and v5.0.1 on 2026-03-05. A major version landed this year, which means upgrade work is real rather than theoretical. The v6 change to prototype resolution is the kind of thing that fails silently in a template that reads an inherited property, so read the release notes before bumping. The repository carries RELEASE_NOTES_v4.md and RELEASE_NOTES_v5.md, so release notes are a maintained artifact here.

The licence is Apache-2.0, declared in package.json and shipped as a LICENSE file at the repository root. Apache-2.0 is permissive and includes an explicit patent grant, which is the practical difference from MIT for some legal teams. It also carries notice and attribution obligations when you redistribute the code. This is a description of the licence text, not legal advice; if your organization has a policy on permissive licences with patent clauses, route it through whoever owns that policy.

Upgrade cost is mostly about the options you have already set. If you use _with, strict, unsafePrototypeLocals or destructuredLocals, a major version can change defaults under you. The README documents destructuredLocals as an array of locals that are always destructured from the locals object and remain available even in strict mode, which is the escape hatch for templates that need a named local without the with() construct.

Editorial conclusion

Adopt EJS when your templates are authored by your own team and the data is validated before it reaches render, which is the split the README draws. Do not adopt it as a sandbox for templates written by end users, because the project states plainly that it is effectively a JavaScript runtime and that unfettered access to render is inherently un-secure. Before you commit, verify two things in your own code: whether any route passes req.query or a request body directly into render, and whether you rely on inherited properties from class instances as locals, since unsafePrototypeLocals defaults to false and turning it on disables the v6 prototype-pollution mitigation.

Frequently asked questions

What is EJS and why is it used?

EJS is an embedded JavaScript template engine: templates are mostly HTML with JavaScript in tags, and they compile into functions that return a rendered string. It is used because there is no separate template language to learn and because it complies with the Express view system.

What does EJS stand for?

Embedded JavaScript. The repository description and README title both give the project as Embedded JavaScript templates.

How do I install EJS?

Run npm install ejs. The package supports both import and require, and the CLI, exposed as the ejs binary, requires Node v8 or newer.

What is the difference between JSX and EJS?

EJS files are HTML with JavaScript embedded in tags and render to a string with no component model. JSX files are JavaScript with markup embedded in them and produce a component tree that a framework reconciles.

Official sources

  1. Issues
  2. License: Apache-2.0
  3. mde/ejs on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/mde-ejs.svg)](https://hysenlabs.com/projects/mde-ejs)