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, whileexpress.staticserves 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') %>
| Part | Meaning |
|---|---|
<%- | 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
.ejsextension needed — EJS adds it automatically - Subfolders work:
'partials/header'looks forviews/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>© <%= 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 disk | URL 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>© <%= 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
| Mistake | Symptom | Fix |
|---|---|---|
Missing express.static() middleware | CSS/JS/images return 404 | Add app.use(express.static('public')) |
| Using relative paths in templates | Works on / but breaks on /admin/users | Use absolute paths: /css/style.css |
Including public/ in the URL | 404 for /public/css/style.css | Remove public/ — it is the root |
| Static middleware after routes | Routes shadow static files | Place express.static() before route definitions |
Wrong __dirname path | Files not found when starting from different directory | Use path.join(__dirname, 'public') |
13. Key Takeaways
- Partials eliminate duplication — write shared HTML once,
<%- include() %>everywhere. - Use
<%- %>(not<%= %>) for includes — partial content is HTML that must not be escaped. - Pass extra data to partials with the second argument:
include('card', { item }). express-ejs-layoutsadds a<%- body %>layout system for cleaner page files.express.static('public')serves CSS, JS, and images from thepublic/folder.- Always use absolute paths (
/css/style.css) in templates — never relative.
Explain-It Challenge
Explain without notes:
- Why do you use
<%- include() %>with a dash instead of<%= include() %>? - If you change the copyright year in
footer.ejs, how many files do you need to edit? - 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