EJS Templating Syntax
Objectives
By the end of this lesson, you should be able to:
- Use
<% %>to run JavaScript logic inside a template, without printing output - Loop over an array with
.forEach()inside a template - Use an
if/elseinside a template to conditionally render content
💡 Why this matters: A real page rarely shows one fixed value, it shows a list of products, a conditional “in stock” badge, an empty state when there’s nothing to show. EJS’s control-flow tags handle exactly this, using plain JavaScript, no separate templating language to learn.
⚠️ A note on verification: every snippet and every response shown below was actually run and tested with real HTTP requests.
Scriptlet Tags: <% %>
EJS has two tag types: <%= expression %> (Lesson 1) evaluates expression and prints the result, <% statement %> runs statement as plain JavaScript but prints nothing, used for loops, conditionals, and any logic that doesn’t itself produce a value to display.
Looping Over an Array
<!-- views/products.ejs -->
<h1>Products</h1>
<% if (products.length === 0) { %>
<p>No products found.</p>
<% } else { %>
<ul>
<% products.forEach(function(product) { %>
<li>
<%= product.name %> - $<%= product.price %>
<% if (product.inStock) { %>
<strong>(in stock)</strong>
<% } else { %>
(out of stock)
<% } %>
</li>
<% }); %>
</ul>
<% } %>
const products = [
{ name: 'Keyboard', price: 45, inStock: true },
{ name: 'Monitor', price: 210, inStock: false }
];
app.get('/products', (req, res) => {
res.render('products', { products });
});
curl http://localhost:4102/products
<h1>Products</h1>
<ul>
<li>Keyboard - $45 <strong>(in stock)</strong></li>
<li>Monitor - $210 (out of stock)</li>
</ul>
This is a genuine JavaScript if/else and .forEach() (Module 2’s array methods), split across <% %> tags with regular HTML in between, EJS templates are, quite literally, JavaScript with an HTML-templating syntax layered on top, nothing new to learn beyond the tag syntax itself.
Empty States
curl http://localhost:4102/products-empty
<h1>Products</h1>
<p>No products found.</p>
Passing an empty array renders the if (products.length === 0) branch instead, exactly like a normal JavaScript conditional, this is the standard pattern for handling “no results” states in a real application.
Escaped vs Unescaped Output
<p>Escaped: <%= userInput %></p>
<p>Unescaped: <%- userInput %></p>
res.render('escaping', { userInput: '<strong>bold</strong>' });
<p>Escaped: <strong>bold</strong></p>
<p>Unescaped: <strong>bold</strong></p>
<%= %> HTML-escapes its output automatically, < becomes <, and so on, this is a critical security default, it prevents a value containing HTML or a <script> tag from being interpreted as real markup (this class of vulnerability is called Cross-Site Scripting, or XSS). <%- %> outputs raw, unescaped HTML instead, only appropriate for content you trust completely, like the partials in the next lesson, never for raw user input.
Try It
- Render a template looping over an array of at least three names, printing each in an
<li>. - Add an
if/elseinside the loop that shows different text depending on a boolean property of each item (likeinStockabove). - Pass an empty array to the same template, and confirm an appropriate “nothing here” message renders instead of an empty list.
- Render a value containing
<em>tags with both<%= %>and<%- %>, and explain, in your own words, why the escaped version is the safer default for anything coming from user input.
Recap
<% statement %>runs JavaScript without printing, used for loops and conditionals,<%= expression %>prints an escaped value..forEach()andif/elseinside<% %>tags work exactly like normal JavaScript, split across HTML.<%= %>escapes HTML automatically (safe default, prevents XSS),<%- %>outputs raw HTML, only for trusted content.
Next lesson: passing more complex data into a view.