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/else for conditional rendering, forEach and for for looping, and switch for 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

MistakeWhat HappensFix
Missing { or }Syntax error or unexpected outputEvery <% if (...) { %> needs a matching <% } %>
Using <%= if %>Prints "undefined" — if is a statement, not an expressionUse <% if %> (no =)
Forgetting ); in forEachSyntaxError<% }); %> — note the closing paren and semicolon
Checking undefined variableReferenceErrorUse locals.varName instead
Nesting without indentationWorks but unreadableIndent consistently

12. Key Takeaways

  1. Control flow in EJS is plain JavaScript — if you know JS, you know EJS logic.
  2. <% %> scriptlets wrap the opening and closing of every if, for, and forEach.
  3. Always check optional variables with locals.variableName to avoid ReferenceError.
  4. forEach is the go-to loop; use for when you need break, continue, or index math.
  5. Keep complex logic in the route handler — pass simple, ready-to-render data to templates.

Explain-It Challenge

Explain without notes:

  1. Write the EJS code to loop through an array of items and render each as a <li>.
  2. How would you conditionally show an "Admin Panel" link only if user.role === 'admin'?
  3. 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 ->