Episode 3 — NodeJS MongoDB Backend Architecture / 3.5 — Template Engine EJS

3.5.e — Layouts, Partials & Static Files

In one sentence: Partials let you write shared UI pieces (header, footer, navbar) once and include() them everywhere, while express.static serves your CSS, JavaScript, and images — together they turn a collection of EJS files into a real, maintainable multi-page website.

Navigation: <- 3.5.d — Control Flow in EJS | 3.5 Overview


1. What Are Partials?

A partial is a small EJS file that contains a reusable chunk of HTML — a header, a footer, a navbar, a card component. Instead of copy-pasting the same HTML into every page, you write it once and include it.

Without partials:                    With partials:
┌──────────────┐                    ┌──────────────┐
│ home.ejs     │                    │ home.ejs     │
│  - header    │                    │  include(hdr)│
│  - content   │                    │  - content   │
│  - footer    │                    │  include(ftr)│
├──────────────┤                    ├──────────────┤
│ about.ejs    │                    │ about.ejs    │
│  - header    │  ← duplicated!    │  include(hdr)│
│  - content   │                    │  - content   │
│  - footer    │                    │  include(ftr)│
└──────────────┘                    └──────────────┘
                                      header.ejs ← single source
                                      footer.ejs ← single source

Benefits:

  • DRY — change the nav once, every page updates
  • Consistency — guaranteed identical headers and footers
  • Smaller files — each template focuses on its own content

2. Basic Include Syntax

<%- include('partials/header') %>
PartMeaning
<%-Unescaped output (the partial contains HTML — must not be escaped)
include(EJS function that reads and compiles another file
'partials/header'Path relative to the views/ folder
)Close function call
%>Close EJS tag

Path rules:

  • Paths are relative to the views/ directory
  • No .ejs extension needed — EJS adds it automatically
  • Subfolders work: 'partials/header' looks for views/partials/header.ejs

3. Creating Header, Footer, and Navbar Partials

Directory structure

views/
├── partials/
│   ├── header.ejs
│   ├── navbar.ejs
│   └── footer.ejs
├── home.ejs
├── about.ejs
└── contact.ejs

views/partials/header.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= locals.title || 'My Website' %></title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <%- include('partials/navbar') %>

views/partials/navbar.ejs

<nav class="navbar">
  <div class="nav-brand">
    <a href="/">MySite</a>
  </div>
  <ul class="nav-links">
    <li><a href="/">Home</a></li>
    <li><a href="/about">About</a></li>
    <li><a href="/contact">Contact</a></li>
    <% if (locals.user) { %>
      <li><a href="/dashboard">Dashboard</a></li>
      <li><a href="/logout">Logout (<%= user.name %>)</a></li>
    <% } else { %>
      <li><a href="/login">Login</a></li>
    <% } %>
  </ul>
</nav>

views/partials/footer.ejs

  <footer class="site-footer">
    <p>&copy; <%= new Date().getFullYear() %> MySite. All rights reserved.</p>
  </footer>
</body>
</html>

views/home.ejs (using all three)

<%- include('partials/header') %>

<main class="container">
  <h1>Welcome to Our Site</h1>
  <p>This is the home page content.</p>
</main>

<%- include('partials/footer') %>

views/about.ejs

<%- include('partials/header') %>

<main class="container">
  <h1>About Us</h1>
  <p>We are a team of developers building great things.</p>
</main>

<%- include('partials/footer') %>

Both pages share the same header and footer. Changing the navbar links in navbar.ejs updates every page instantly.


4. Passing Data to Partials

Partials automatically have access to all variables passed by res.render(). But you can also pass extra local data to a specific include.

Syntax

<%- include('partials/card', { item: product, showPrice: true }) %>

The second argument is an object that gets merged into the partial's scope.

Example — Reusable Card Partial

views/partials/card.ejs:

<div class="card">
  <img src="<%= item.image %>" alt="<%= item.name %>">
  <h3><%= item.name %></h3>
  <% if (showPrice) { %>
    <p class="price">$<%= item.price.toFixed(2) %></p>
  <% } %>
  <p><%= item.description %></p>
</div>

views/shop.ejs:

<%- include('partials/header') %>

<main>
  <h1>Our Products</h1>
  <div class="card-grid">
    <% products.forEach(function(product) { %>
      <%- include('partials/card', { item: product, showPrice: true }) %>
    <% }); %>
  </div>
</main>

<%- include('partials/footer') %>

Express route:

app.get('/shop', (req, res) => {
  res.render('shop', {
    title: 'Shop',
    products: [
      { name: 'Laptop', price: 999.99, image: '/img/laptop.jpg', description: '15-inch display' },
      { name: 'Phone', price: 699.99, image: '/img/phone.jpg', description: 'Latest model' },
      { name: 'Tablet', price: 449.99, image: '/img/tablet.jpg', description: '10-inch screen' }
    ]
  });
});

Example — Alert Partial with Type

views/partials/alert.ejs:

<div class="alert alert-<%= type %>">
  <strong><%= type === 'error' ? 'Error!' : 'Notice:' %></strong>
  <%= message %>
</div>

Usage in any page:

<% if (locals.error) { %>
  <%- include('partials/alert', { type: 'error', message: error }) %>
<% } %>

<% if (locals.success) { %>
  <%- include('partials/alert', { type: 'success', message: success }) %>
<% } %>

5. The Layout Pattern (Manual)

The simplest layout approach is the header + content + footer sandwich:

<%- include('partials/header') %>
  <!-- Page-specific content here -->
<%- include('partials/footer') %>

This is what most EJS projects use. The header partial opens <html>, <head>, and <body>. The footer partial closes </body> and </html>.

Passing the page title

Since the <title> tag is inside the header partial, you pass title from each route:

app.get('/', (req, res) => {
  res.render('home', { title: 'Home' });
});

app.get('/about', (req, res) => {
  res.render('about', { title: 'About Us' });
});

The header partial uses it:

<title><%= locals.title || 'Default Title' %></title>

6. express-ejs-layouts — Full Layout Support

For projects that want a true layout file (like layout.ejs that wraps every page), install the express-ejs-layouts package.

Installation

npm install express-ejs-layouts

Setup

const express = require('express');
const expressLayouts = require('express-ejs-layouts');
const app = express();

app.set('view engine', 'ejs');
app.set('views', './views');

// Enable layouts
app.use(expressLayouts);
app.set('layout', 'layouts/main');  // views/layouts/main.ejs

views/layouts/main.ejs (the layout file)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= locals.title || 'My App' %></title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <%- include('../partials/navbar') %>

  <main class="container">
    <%- body %>
  </main>

  <%- include('../partials/footer') %>

  <script src="/js/main.js"></script>
</body>
</html>

The key is <%- body %> — this is where each page's content gets injected.

views/home.ejs (just the page content)

<h1>Welcome Home</h1>
<p>This content gets placed where <%- body %> is in the layout.</p>

views/about.ejs

<h1>About Us</h1>
<p>Our story begins in 2020...</p>

Notice: Page files no longer need <!DOCTYPE>, <head>, or include statements. The layout handles all of that. Each page file is just its unique content.

Routes stay the same

app.get('/', (req, res) => {
  res.render('home', { title: 'Home' });
});

app.get('/about', (req, res) => {
  res.render('about', { title: 'About Us' });
});

7. Serving Static Files with express.static

Static files (CSS, client-side JS, images, fonts) are not templates — they are served as-is. Express has built-in middleware for this.

Setup

const path = require('path');

// Serve files from the 'public' folder
app.use(express.static(path.join(__dirname, 'public')));

Directory structure

project/
├── app.js
├── public/               <-- Static files
│   ├── css/
│   │   └── style.css
│   ├── js/
│   │   └── main.js
│   └── img/
│       ├── logo.png
│       └── hero.jpg
└── views/                <-- EJS templates
    ├── partials/
    └── home.ejs

How paths work

The public/ folder becomes the root for static file URLs:

File on diskURL in browser
public/css/style.css/css/style.css
public/js/main.js/js/main.js
public/img/logo.png/img/logo.png

Notice: the public/ prefix is not part of the URL. Express strips it.


8. Linking Static Files in EJS Templates

CSS

<link rel="stylesheet" href="/css/style.css">

JavaScript

<script src="/js/main.js"></script>

Images

<img src="/img/logo.png" alt="Logo">

Favicon

<link rel="icon" href="/img/favicon.ico">

Always use absolute paths (starting with /) in EJS templates. Relative paths (css/style.css) break when the page URL has nested segments (/admin/users).


9. Full Project Example — Multi-Page Website

Here is a complete project structure with shared layout, partials, static files, and multiple pages.

Directory structure

portfolio-site/
├── app.js
├── package.json
├── public/
│   ├── css/
│   │   └── style.css
│   └── img/
│       └── profile.jpg
└── views/
    ├── partials/
    │   ├── header.ejs
    │   ├── navbar.ejs
    │   └── footer.ejs
    ├── home.ejs
    ├── projects.ejs
    ├── contact.ejs
    └── 404.ejs

app.js

const express = require('express');
const path = require('path');
const app = express();
const PORT = 3000;

// Configuration
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.use(express.static(path.join(__dirname, 'public')));
app.use(express.urlencoded({ extended: true }));  // For form data

// Sample data (normally from a database)
const projects = [
  { id: 1, name: 'E-Commerce API', tech: 'Node, Express, MongoDB', status: 'Complete' },
  { id: 2, name: 'Blog Platform', tech: 'EJS, Express, PostgreSQL', status: 'In Progress' },
  { id: 3, name: 'Chat Application', tech: 'Socket.io, React', status: 'Planning' }
];

// Routes
app.get('/', (req, res) => {
  res.render('home', {
    title: 'Home',
    name: 'Arjun Sharma',
    tagline: 'Full-Stack Developer'
  });
});

app.get('/projects', (req, res) => {
  res.render('projects', {
    title: 'Projects',
    projects: projects
  });
});

app.get('/contact', (req, res) => {
  res.render('contact', { title: 'Contact' });
});

app.post('/contact', (req, res) => {
  const { name, email, message } = req.body;
  console.log('Contact form:', { name, email, message });
  res.render('contact', {
    title: 'Contact',
    success: 'Message sent successfully!'
  });
});

// 404 handler — must be last
app.use((req, res) => {
  res.status(404).render('404', { title: '404 Not Found' });
});

app.listen(PORT, () => {
  console.log(`Server running at http://localhost:${PORT}`);
});

views/partials/header.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title><%= locals.title || 'Portfolio' %> | Arjun Sharma</title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  <%- include('navbar') %>
  <main class="container">

views/partials/navbar.ejs

<nav class="navbar">
  <a href="/" class="brand">Arjun.dev</a>
  <ul>
    <li><a href="/">Home</a></li>
    <li><a href="/projects">Projects</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

views/partials/footer.ejs

  </main>
  <footer>
    <p>&copy; <%= new Date().getFullYear() %> Arjun Sharma. Built with Express + EJS.</p>
  </footer>
</body>
</html>

views/home.ejs

<%- include('partials/header') %>

  <section class="hero">
    <img src="/img/profile.jpg" alt="<%= name %>" class="avatar">
    <h1><%= name %></h1>
    <p><%= tagline %></p>
  </section>

<%- include('partials/footer') %>

views/projects.ejs

<%- include('partials/header') %>

  <h1>My Projects</h1>

  <% if (projects.length === 0) { %>
    <p>No projects yet. Check back soon!</p>
  <% } else { %>
    <div class="project-grid">
      <% projects.forEach(function(project) { %>
        <div class="project-card">
          <h3><%= project.name %></h3>
          <p><strong>Tech:</strong> <%= project.tech %></p>
          <span class="status status-<%= project.status.toLowerCase().replace(' ', '-') %>">
            <%= project.status %>
          </span>
        </div>
      <% }); %>
    </div>
  <% } %>

<%- include('partials/footer') %>

views/contact.ejs

<%- include('partials/header') %>

  <h1>Get in Touch</h1>

  <% if (locals.success) { %>
    <div class="alert alert-success"><%= success %></div>
  <% } %>

  <form method="POST" action="/contact" class="contact-form">
    <div class="form-group">
      <label for="name">Name</label>
      <input type="text" id="name" name="name" required>
    </div>
    <div class="form-group">
      <label for="email">Email</label>
      <input type="email" id="email" name="email" required>
    </div>
    <div class="form-group">
      <label for="message">Message</label>
      <textarea id="message" name="message" rows="5" required></textarea>
    </div>
    <button type="submit">Send Message</button>
  </form>

<%- include('partials/footer') %>

views/404.ejs

<%- include('partials/header') %>

  <div class="error-page">
    <h1>404</h1>
    <p>The page you are looking for does not exist.</p>
    <a href="/">Go back home</a>
  </div>

<%- include('partials/footer') %>

public/css/style.css (sample)

* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
.navbar { background: #2c3e50; padding: 15px 30px; display: flex; justify-content: space-between; align-items: center; }
.navbar a { color: #ecf0f1; text-decoration: none; }
.navbar ul { list-style: none; display: flex; gap: 20px; }
.hero { text-align: center; padding: 60px 0; }
.avatar { width: 150px; height: 150px; border-radius: 50%; }
.project-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; }
.project-card { border: 1px solid #ddd; padding: 20px; border-radius: 8px; }
.alert-success { background: #d4edda; color: #155724; padding: 12px; border-radius: 4px; margin-bottom: 20px; }
footer { text-align: center; padding: 20px; margin-top: 40px; border-top: 1px solid #eee; color: #666; }

10. Partial Nesting — Partials Inside Partials

Partials can include other partials. The header partial in the example above includes the navbar partial:

<!-- header.ejs includes navbar.ejs -->
<%- include('navbar') %>

There is no limit to nesting depth, but keep it reasonable (2-3 levels). Deep nesting makes templates hard to debug.


11. Conditional Partials

You can conditionally include different partials:

<% if (locals.user && user.role === 'admin') { %>
  <%- include('partials/admin-sidebar') %>
<% } else { %>
  <%- include('partials/user-sidebar') %>
<% } %>

12. Common Static File Mistakes

MistakeSymptomFix
Missing express.static() middlewareCSS/JS/images return 404Add app.use(express.static('public'))
Using relative paths in templatesWorks on / but breaks on /admin/usersUse absolute paths: /css/style.css
Including public/ in the URL404 for /public/css/style.cssRemove public/ — it is the root
Static middleware after routesRoutes shadow static filesPlace express.static() before route definitions
Wrong __dirname pathFiles not found when starting from different directoryUse path.join(__dirname, 'public')

13. Key Takeaways

  1. Partials eliminate duplication — write shared HTML once, <%- include() %> everywhere.
  2. Use <%- %> (not <%= %>) for includes — partial content is HTML that must not be escaped.
  3. Pass extra data to partials with the second argument: include('card', { item }).
  4. express-ejs-layouts adds a <%- body %> layout system for cleaner page files.
  5. express.static('public') serves CSS, JS, and images from the public/ folder.
  6. Always use absolute paths (/css/style.css) in templates — never relative.

Explain-It Challenge

Explain without notes:

  1. Why do you use <%- include() %> with a dash instead of <%= include() %>?
  2. If you change the copyright year in footer.ejs, how many files do you need to edit?
  3. Why does <link href="css/style.css"> work on the homepage but break on /about/team?

Navigation: <- 3.5.d — Control Flow in EJS | 3.5 Overview