Episode 3 — NodeJS MongoDB Backend Architecture / 3.5 — Template Engine EJS
3.5.d — Control Flow in EJS
In one sentence: EJS templates use plain JavaScript for control flow —
if/elsefor conditional rendering,forEachandforfor looping, andswitchfor multi-branch logic — all wrapped in<% %>scriptlet tags with HTML between the opening and closing braces.
Navigation: <- 3.5.c — EJS Syntax Deep Dive | 3.5.e — Layouts, Partials & Static Files ->
1. The Pattern — How Control Flow Works in EJS
Every control structure follows the same pattern:
<% OPENING STATEMENT { %>
HTML content here (with optional <%= output %> tags)
<% } %>
The JavaScript braces { } define the block. The HTML between them is rendered (or skipped) based on the condition. This is regular JavaScript — the only new thing is that the braces can span across HTML lines.
2. Conditionals — if / else if / else
Basic if
Express route:
app.get('/profile', (req, res) => {
res.render('profile', {
user: { name: 'Arjun', isVerified: true }
});
});
views/profile.ejs:
<h1><%= user.name %></h1>
<% if (user.isVerified) { %>
<span class="badge badge-success">Verified Account</span>
<% } %>
If user.isVerified is true, the badge HTML appears. If false, it is completely absent from the output — not hidden, not display:none, just not in the HTML at all.
if / else
<% if (user.isVerified) { %>
<span class="badge-success">Verified</span>
<% } else { %>
<span class="badge-warning">Unverified — Please check your email</span>
<% } %>
if / else if / else
Express route:
app.get('/order/:id', (req, res) => {
res.render('order', {
order: { id: 'ORD-1234', status: 'shipped' }
});
});
views/order.ejs:
<h2>Order #<%= order.id %></h2>
<% if (order.status === 'pending') { %>
<div class="status yellow">Order is being processed...</div>
<% } else if (order.status === 'shipped') { %>
<div class="status blue">Your order is on the way!</div>
<% } else if (order.status === 'delivered') { %>
<div class="status green">Delivered successfully</div>
<% } else { %>
<div class="status red">Unknown status</div>
<% } %>
3. Loops — forEach
forEach is the most common loop in EJS templates because it reads naturally and avoids index management.
Express route:
app.get('/blog', (req, res) => {
res.render('blog', {
posts: [
{ title: 'Getting Started with Node', date: '2025-01-15', excerpt: 'Learn the basics...' },
{ title: 'Express Routing Guide', date: '2025-02-20', excerpt: 'Master routes...' },
{ title: 'MongoDB for Beginners', date: '2025-03-10', excerpt: 'NoSQL basics...' }
]
});
});
views/blog.ejs:
<h1>Blog</h1>
<% if (posts.length === 0) { %>
<p>No posts yet.</p>
<% } else { %>
<% posts.forEach(function(post) { %>
<article class="post-card">
<h2><%= post.title %></h2>
<time><%= post.date %></time>
<p><%= post.excerpt %></p>
</article>
<% }); %>
<% } %>
Rendered HTML:
<h1>Blog</h1>
<article class="post-card">
<h2>Getting Started with Node</h2>
<time>2025-01-15</time>
<p>Learn the basics...</p>
</article>
<article class="post-card">
<h2>Express Routing Guide</h2>
<time>2025-02-20</time>
<p>Master routes...</p>
</article>
<article class="post-card">
<h2>MongoDB for Beginners</h2>
<time>2025-03-10</time>
<p>NoSQL basics...</p>
</article>
forEach with index
<ol>
<% users.forEach(function(user, index) { %>
<li>
<strong>#<%= index + 1 %></strong> — <%= user.name %>
</li>
<% }); %>
</ol>
forEach with arrow functions
<% products.forEach((product) => { %>
<div class="product">
<h3><%= product.name %></h3>
<p>$<%= product.price.toFixed(2) %></p>
</div>
<% }); %>
4. Loops — for Loop
Use a for loop when you need the index variable or want break/continue control.
Express route:
app.get('/leaderboard', (req, res) => {
res.render('leaderboard', {
players: [
{ name: 'Priya', score: 2500 },
{ name: 'Arjun', score: 2350 },
{ name: 'Rahul', score: 2200 },
{ name: 'Meera', score: 2100 },
{ name: 'Dev', score: 1900 }
]
});
});
views/leaderboard.ejs:
<h1>Top 3 Players</h1>
<table>
<thead>
<tr><th>Rank</th><th>Player</th><th>Score</th></tr>
</thead>
<tbody>
<% for (let i = 0; i < Math.min(players.length, 3); i++) { %>
<tr>
<td><%= i + 1 %></td>
<td><%= players[i].name %></td>
<td><%= players[i].score %></td>
</tr>
<% } %>
</tbody>
</table>
for...of loop
<ul>
<% for (const item of items) { %>
<li><%= item.name %> — $<%= item.price %></li>
<% } %>
</ul>
for...in loop (for object keys)
<%
const settings = { theme: 'dark', language: 'en', notifications: 'on' };
%>
<table>
<% for (const key in settings) { %>
<tr>
<td><strong><%= key %></strong></td>
<td><%= settings[key] %></td>
</tr>
<% } %>
</table>
5. Nested Conditionals and Loops
Real-world templates often combine loops with conditionals.
Express route:
app.get('/team', (req, res) => {
res.render('team', {
departments: [
{
name: 'Engineering',
members: [
{ name: 'Arjun', role: 'Lead', active: true },
{ name: 'Priya', role: 'Senior Dev', active: true },
{ name: 'Dev', role: 'Intern', active: false }
]
},
{
name: 'Design',
members: [
{ name: 'Meera', role: 'UI Lead', active: true },
{ name: 'Rahul', role: 'UX Researcher', active: true }
]
}
]
});
});
views/team.ejs:
<h1>Our Team</h1>
<% departments.forEach(function(dept) { %>
<section class="department">
<h2><%= dept.name %> (<%= dept.members.length %> members)</h2>
<% if (dept.members.length === 0) { %>
<p>No members in this department yet.</p>
<% } else { %>
<ul>
<% dept.members.forEach(function(member) { %>
<li class="<%= member.active ? 'active' : 'inactive' %>">
<strong><%= member.name %></strong> — <%= member.role %>
<% if (!member.active) { %>
<em>(On leave)</em>
<% } %>
</li>
<% }); %>
</ul>
<% } %>
</section>
<% }); %>
6. Using locals to Check if a Variable Exists
When a variable might or might not be passed to the template, always use locals:
Express routes:
// Route 1 — passes an error
app.get('/login', (req, res) => {
res.render('login', {
error: 'Invalid email or password'
});
});
// Route 2 — no error
app.get('/login', (req, res) => {
res.render('login'); // No data passed at all
});
views/login.ejs:
<h1>Login</h1>
<%# WRONG — crashes when 'error' was not passed %>
<%# <% if (error) { %> %>
<%# CORRECT — locals.error is safely undefined %>
<% if (locals.error) { %>
<div class="alert alert-danger">
<strong>Error:</strong> <%= error %>
</div>
<% } %>
<% if (locals.success) { %>
<div class="alert alert-success">
<strong>Success:</strong> <%= success %>
</div>
<% } %>
<form method="POST" action="/login">
<input type="email" name="email" placeholder="Email" required>
<input type="password" name="password" placeholder="Password" required>
<button type="submit">Log In</button>
</form>
7. Switch Statements in EJS
For multiple branches based on a single value, switch can be cleaner than long if/else if chains.
Express route:
app.get('/ticket/:id', (req, res) => {
res.render('ticket', {
ticket: {
id: 'TKT-567',
priority: 'high',
title: 'Login page broken on mobile'
}
});
});
views/ticket.ejs:
<h2>Ticket: <%= ticket.title %></h2>
<% switch (ticket.priority) { %>
<% case 'low': %>
<span class="badge bg-info">Low Priority</span>
<% break; %>
<% case 'medium': %>
<span class="badge bg-warning">Medium Priority</span>
<% break; %>
<% case 'high': %>
<span class="badge bg-danger">High Priority</span>
<% break; %>
<% case 'critical': %>
<span class="badge bg-dark text-white">CRITICAL</span>
<% break; %>
<% default: %>
<span class="badge bg-secondary">Unknown</span>
<% } %>
Note: Switch statements in EJS work but look awkward. Many developers prefer if/else if or compute the badge class in the route handler and pass it as data.
8. Real Example — User List with Roles
Express route:
app.get('/admin/users', (req, res) => {
res.render('admin/users', {
pageTitle: 'User Management',
currentUser: { name: 'Admin', role: 'superadmin' },
users: [
{ id: 1, name: 'Arjun Sharma', email: 'arjun@example.com', role: 'admin', active: true },
{ id: 2, name: 'Priya Patel', email: 'priya@example.com', role: 'editor', active: true },
{ id: 3, name: 'Rahul Kumar', email: 'rahul@example.com', role: 'viewer', active: false },
{ id: 4, name: 'Meera Singh', email: 'meera@example.com', role: 'editor', active: true }
]
});
});
views/admin/users.ejs:
<!DOCTYPE html>
<html>
<head><title><%= pageTitle %></title></head>
<body>
<h1><%= pageTitle %></h1>
<p>Logged in as: <strong><%= currentUser.name %></strong></p>
<table border="1" cellpadding="8">
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th>Status</th>
<% if (currentUser.role === 'superadmin') { %>
<th>Actions</th>
<% } %>
</tr>
</thead>
<tbody>
<% users.forEach(function(user) { %>
<tr class="<%= user.active ? '' : 'text-muted' %>">
<td><%= user.id %></td>
<td><%= user.name %></td>
<td><%= user.email %></td>
<td>
<% if (user.role === 'admin') { %>
<span class="badge-admin">Admin</span>
<% } else if (user.role === 'editor') { %>
<span class="badge-editor">Editor</span>
<% } else { %>
<span class="badge-viewer">Viewer</span>
<% } %>
</td>
<td><%= user.active ? 'Active' : 'Disabled' %></td>
<% if (currentUser.role === 'superadmin') { %>
<td>
<a href="/admin/users/<%= user.id %>/edit">Edit</a>
<% if (user.active) { %>
| <a href="/admin/users/<%= user.id %>/disable">Disable</a>
<% } else { %>
| <a href="/admin/users/<%= user.id %>/enable">Enable</a>
<% } %>
</td>
<% } %>
</tr>
<% }); %>
</tbody>
</table>
<% if (users.length === 0) { %>
<p>No users found.</p>
<% } else { %>
<p>Showing <strong><%= users.length %></strong> user(s).</p>
<% } %>
</body>
</html>
9. Real Example — Conditional Navigation (Admin vs User)
Express route:
app.get('/dashboard', (req, res) => {
res.render('dashboard', {
user: { name: 'Priya', role: 'admin' }
});
});
views/partials/nav.ejs:
<nav>
<a href="/">Home</a>
<a href="/dashboard">Dashboard</a>
<% if (user.role === 'admin' || user.role === 'superadmin') { %>
<a href="/admin/users">Manage Users</a>
<a href="/admin/settings">Settings</a>
<a href="/admin/logs">View Logs</a>
<% } %>
<% if (user.role === 'editor' || user.role === 'admin') { %>
<a href="/content/new">Create Content</a>
<a href="/content/drafts">My Drafts</a>
<% } %>
<div class="nav-right">
<span>Hello, <%= user.name %></span>
<a href="/logout">Log Out</a>
</div>
</nav>
Different roles see different navigation links — all handled server-side before the HTML reaches the browser.
10. Real Example — Product Grid with "Out of Stock" Overlay
Express route:
app.get('/shop', (req, res) => {
res.render('shop', {
products: [
{ name: 'Wireless Mouse', price: 29.99, stock: 15, image: '/img/mouse.jpg' },
{ name: 'USB-C Hub', price: 49.99, stock: 0, image: '/img/hub.jpg' },
{ name: 'Mechanical Keyboard', price: 89.99, stock: 3, image: '/img/keyboard.jpg' }
]
});
});
views/shop.ejs:
<h1>Shop</h1>
<div class="product-grid">
<% products.forEach(function(product) { %>
<div class="product-card <%= product.stock === 0 ? 'out-of-stock' : '' %>">
<img src="<%= product.image %>" alt="<%= product.name %>">
<h3><%= product.name %></h3>
<p class="price">$<%= product.price.toFixed(2) %></p>
<% if (product.stock === 0) { %>
<p class="stock-label red">Out of Stock</p>
<button disabled>Unavailable</button>
<% } else if (product.stock <= 5) { %>
<p class="stock-label orange">Only <%= product.stock %> left!</p>
<button>Add to Cart</button>
<% } else { %>
<p class="stock-label green">In Stock</p>
<button>Add to Cart</button>
<% } %>
</div>
<% }); %>
</div>
11. Common Control Flow Mistakes
| Mistake | What Happens | Fix |
|---|---|---|
Missing { or } | Syntax error or unexpected output | Every <% if (...) { %> needs a matching <% } %> |
Using <%= if %> | Prints "undefined" — if is a statement, not an expression | Use <% if %> (no =) |
Forgetting ); in forEach | SyntaxError | <% }); %> — note the closing paren and semicolon |
| Checking undefined variable | ReferenceError | Use locals.varName instead |
| Nesting without indentation | Works but unreadable | Indent consistently |
12. Key Takeaways
- Control flow in EJS is plain JavaScript — if you know JS, you know EJS logic.
<% %>scriptlets wrap the opening and closing of everyif,for, andforEach.- Always check optional variables with
locals.variableNameto avoidReferenceError. forEachis the go-to loop; useforwhen you needbreak,continue, or index math.- Keep complex logic in the route handler — pass simple, ready-to-render data to templates.
Explain-It Challenge
Explain without notes:
- Write the EJS code to loop through an array of
itemsand render each as a<li>. - How would you conditionally show an "Admin Panel" link only if
user.role === 'admin'? - Why is
<% if (message) { %>dangerous but<% if (locals.message) { %>safe?
Navigation: <- 3.5.c — EJS Syntax Deep Dive | 3.5.e — Layouts, Partials & Static Files ->