EJS Cheatsheet - Tags, Includes & Express Setup
This reference is for Node and Express developers who render pages serverside with EJS, the template engine you configure in one line and use everywhere. It covers the tag syntax (<% code %>, <%= escaped output %>, <%- raw HTML %>, <%- include %>, comments), how to branch and loop over the locals you pass in, splitting pages into includes, sharing a layout, and calling res.render from Express. Unlike a generic EJS primer, entries are grouped by what each part of the template does, and note when output is escaped vs raw. After reading you should be able to build an Express view, loop over data with <%- for %>, escape user content with <%=, and reuse header/footer via includes.
Basic Syntax 5
<% code %><%= value %><%- value %><%# comment %><%_ code %>Conditionals 3
<% if (condition) { %>...<% } %><% if (condition) { %>...<% } else { %>...<% } %><% if (a) { %>...<% } else if (b) { %>...<% } else { %>...<% } %>Loops 4
<% for (let i = 0; i < items.length; i++) { %>...<% } %><% items.forEach(item => { %>...<% }) %><% for (const item of items) { %>...<% } %><% for (const key in obj) { %>...<% } %>Includes 4
<%- include("header") %><%- include("footer") %><%- include("sidebar", { user }) %><%- include("partials/nav") %>Layout & Inheritance 3
<%- body %><% layout("layout") -%><%- include("header", {title: "Home"}) %>Common Tricks 5
<%= user?.name || "Guest" %><%= JSON.stringify(data) %><%= new Date().toLocaleDateString() %><%= items.join(", ") %><%= Math.round(price * 100) / 100 %>Express Integration 4
app.set("view engine", "ejs")res.render("index", { title: "Home" })app.set("views", "./views")res.render("users/list", { users })Render API & Config 7
ejs.render(str, data)ejs.renderFile("tpl.ejs", data, (err, html) => {})ejs.compile(str, { client: true })ejs.render(str, data, { cache: true })ejs.delimiter = "$"<%= locals.user %>const fn = ejs.compile(str); fn(data)Command Examples
A loop that outputs user data through <%= %>
<% items.forEach(item => { %>
<%= item.title %>
<% }) %>item.title passes through <%= %>, so any angle brackets inside are escaped and user input cannot inject markup. The surrounding forEach uses <% %> purely for execution, and its newlines do land in the HTML; switch to <%_ %> when you want compact output.
Escaped versus raw output
<%- include("header", {title: "Home"}) %>
<%= user?.name || "Guest" %>
<%- post.html %>The first line uses <%- %> because include returns already-rendered HTML; the user name goes through <%= %> so it is escaped; post.html is your own stored content and only then earns <%- %>. Mixing all three carelessly is where XSS creeps in.
Three conditional forms and whitespace control
<% if (a) { %>...<% } else if (b) { %>...<% } else { %>...<% } %>
<%_ code %>
<%= user?.name || "Guest" %>The standard multi-branch form elides its middle with "...". Switching to the <%_ %> variant strips whitespace around the control tags so no blank lines leak into the output. Keep conditionals for presentation choices and data source selection, not business logic.
Rendering a template from a subdirectory with data from Express
app.set("view engine", "ejs")
app.set("views", "./views")
res.render("users/list", { users })views sets the root, and the path passed to res.render is relative to it. Inside the template users is reachable as <%= locals.users %>, which is the same as writing users directly. After changing the views directory, remember to keep the static asset paths in your deploy configuration aligned.
Common Pitfalls
- <%- %> performs no escaping whatsoever, so writing any user-derived value there is an open XSS hole. Default to <%= %> for new fields and only downgrade once you are certain the content is your own HTML.
- include loads synchronously: a missing or misnamed target throws at render time rather than silently yielding nothing, and relative paths resolve against the including template’s own directory.
- ejs.delimiter = "$" is a global setting that affects every subsequent render. If only some templates need a custom delimiter, make sure it has not leaked into the other views.
- Use <%# %> for template comments since they emit nothing. HTML comments survive into the served page and therefore disclose content to clients — avoid putting anything sensitive there.
- With view cache enabled in production, template edits stop taking effect immediately. When hot reload stops working, check the cache configuration before restarting processes repeatedly.
Tips
- <%= %> auto-escapes HTML to prevent XSS; <%- %> outputs raw HTML without escaping.
- include loads synchronously, inserting the included template at the current position.
- EJS supports full JavaScript syntax inside <% %>.
- Layout inheritance needs the express-ejs-layouts middleware, or include header/footer manually.
- Cache templates for performance: app.set('view cache', true).
- Disable cache in dev: app.set('view cache', false) so edits apply immediately.
- Functions from ejs.compile run in the browser, fitting client-side rendering.
FAQ
What exactly separates <%= %> from <%- %>, and why not just use <%- %> everywhere?
The difference is HTML escaping. <%= %> converts characters such as <, > and & into entities before writing the value, so user-submitted content is never parsed as markup; <%- %> injects the HTML verbatim. That is the XSS boundary: <%- %> belongs only to HTML you control and have reviewed, while any value originating from users — names, titles, search terms — must go through <%= %>. Getting it wrong does not merely garble the page, it puts script on it that you never wrote.
How does an included template access the data passed to include?
The second argument of include becomes the included template’s locals object. In <%- include("sidebar", { user }) %>, sidebar.ejs can use user directly or reach it as <%= locals.user %> the same way. The included template still sees everything in scope from the parent render, so passing data adds to the scope rather than replacing it — variables you omit remain visible from the parent. That trips people up when shadowing: to genuinely override, pass a key with the same name.
How do I strip the stray blank lines and indentation from rendered HTML?
Use the <%_ %> tag, which discards the surrounding whitespace while still executing the code, specifically to remove visual noise produced by control structures. Layout templates need this most: every newline around <%- include("header") %> ends up in the output, and they accumulate into a large block of meaningless space. Use it consistently as <%_ if (cond) { _%> … <%_ } _%>, trimming both ends of conditional and loop blocks. It removes whitespace rather than indentation semantics, so intricate layout still belongs to CSS.
My template edits are not showing up in development — is it caching?
Almost always. With render caching enabled (the usual production setting) the template string is parsed once and the compiled result is reused. Turn the view cache off in development so edits take effect immediately, and switch it back on when you deploy for the performance. The same applies when rendering strings through ejs.render, where the { cache: true } option governs it. The evidence is usually unambiguous: if identical data looks fresh in staging and stale in production, think cache rather than logic.
How do I choose between the layout mechanism and include for reuse?
Match the granularity. include assembles fragments: headers, footers, sidebars — independent blocks combined with <%- include("header") %> while each page keeps full control of its own structure. Layouts suit a whole-page shell: one template defines the HTML skeleton with a <%- body %> placeholder, child templates fill only the content and declare <% layout("layout") -%>, which requires the express-ejs-layouts middleware. include alone can also fake a layout, but then every page opens and closes it by hand, and pages get forgotten as the count grows.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us