Introduction to Template Engines
Template engines enable you to use static template files in your application. At runtime, the template engine replaces variables in a template file with actual values, and transforms the template into an HTML file sent to the client.
The Bakery Analogy
Template engines work like a bakery:
- Template files are like cookie cutters or cake molds—they define the structure and shape
- Dynamic data is like the dough or batter—the raw material that gets shaped
- Template engine is like the baker who combines the mold and dough
- Generated HTML is the finished product—a uniquely shaped cookie or cake
Just as a bakery can use the same mold to create many different cookies by changing the ingredients or decorations, a template engine can use the same template to create many different HTML pages by changing the data.
Why Use Template Engines?
Template engines provide several benefits for server-rendered applications:
Key Benefits
- Separation of Concerns: Keeps presentation logic separate from business logic
- Code Reusability: Allows reuse of layout sections and components
- Dynamic Content: Easily inject dynamic data into HTML structures
- Maintainability: Easier to maintain and update UI without touching server code
- Improved Developer Experience: Cleaner syntax for generating HTML than string concatenation
When to Use Templates vs. Client-side Rendering
While modern web development often uses client-side frameworks like React or Vue, server-side templates still have important use cases:
- SEO-critical pages: Better search engine optimization with fully rendered HTML
- Content-focused websites: Blogs, news sites, documentation
- Low-interaction interfaces: Static or mostly-static pages
- Initial page load performance: Faster "time to first contentful paint"
- Progressive enhancement: Base functionality works without JavaScript
- Email templates: HTML emails are still generated server-side
- Admin dashboards: Internal applications where SEO isn't a concern
Many modern applications use a hybrid approach: server-side rendering for the initial page load and client-side rendering for subsequent interactions.
Popular Template Engines in Express
Express is compatible with many template engines. Let's look at some of the most popular ones:
| Template Engine | Syntax Style | Features | Advantages | Use Cases |
|---|---|---|---|---|
| EJS | HTML with embedded JavaScript tags |
|
|
Applications where developers need to use JavaScript logic directly in templates |
| Pug (formerly Jade) | Indentation-based minimalist syntax |
|
|
Complex layouts with nested structures and reusable components |
| Handlebars | Logicless templates with curly braces |
|
|
Projects with non-technical template editors or strict MVC separation |
| Nunjucks | Jinja2-inspired syntax |
|
|
Applications with complex template requirements and Python developers familiar with Jinja2 |
The choice of template engine often depends on project requirements, team preferences, and existing skills. In this lecture, we'll focus primarily on EJS and Pug, as they represent different approaches to templating.
Setting Up Template Engines in Express
Express makes it easy to integrate and configure template engines in your application.
Basic Template Engine Setup
const express = require('express');
const app = express();
// Set the view engine
app.set('view engine', 'ejs'); // or 'pug', 'handlebars', etc.
// Set the directory where templates are located
app.set('views', './views');
// Create a route that renders a template
app.get('/', (req, res) => {
res.render('index', {
title: 'Home Page',
message: 'Welcome to our website!',
user: {
name: 'John Doe',
email: 'john@example.com'
},
items: ['Item 1', 'Item 2', 'Item 3']
});
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
In this example:
app.set('view engine', 'ejs')tells Express which template engine to useapp.set('views', './views')specifies the directory where template files are locatedres.render('index', {...})renders the 'index' template with the provided data
Installing Template Engines
Template engines in Express are typically third-party packages that you need to install:
// For EJS
npm install ejs
// For Pug
npm install pug
// For Handlebars (via express-handlebars)
npm install express-handlebars
// For Nunjucks
npm install nunjucks
After installation, Express will automatically require the engine when you set it as your view engine (if it follows Express naming conventions).
Configuring Handlebars with express-handlebars
Some template engines require additional configuration:
const express = require('express');
const { engine } = require('express-handlebars');
const app = express();
// Configure Handlebars
app.engine('handlebars', engine({
defaultLayout: 'main',
layoutsDir: __dirname + '/views/layouts',
partialsDir: __dirname + '/views/partials',
helpers: {
// Custom helpers
formatDate: (date) => {
return new Date(date).toLocaleDateString();
},
uppercase: (text) => {
return text.toUpperCase();
}
}
}));
app.set('view engine', 'handlebars');
app.set('views', './views');
app.get('/', (req, res) => {
res.render('home', {
title: 'Home Page',
content: 'Welcome to our website',
date: new Date()
});
});
app.listen(3000);
In this example, we configure Handlebars with a default layout, specify directories for layouts and partials, and define custom helper functions.
EJS (Embedded JavaScript)
EJS is a simple templating language that lets you generate HTML markup with plain JavaScript. It's a popular choice due to its simplicity and familiarity for those who already know HTML and JavaScript.
Key Features
- Uses
<% %>tags for JavaScript code - Uses
<%= %>for outputting escaped values - Uses
<%- %>for outputting unescaped HTML - Supports includes for partials
- Full JavaScript syntax available in templates
Basic EJS Template Example
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1><%= title %></h1>
<% if (user) { %>
<p>Welcome, <%= user.name %>!</p>
<% } else { %>
<p>Please log in.</p>
<% } %>
</header>
<main>
<p><%= message %></p>
<h2>Items:</h2>
<ul>
<% items.forEach(function(item) { %>
<li><%= item %></li>
<% }); %>
</ul>
</main>
<footer>
<p>© 2025 My Website</p>
</footer>
</body>
</html>
This template shows the basic EJS syntax for:
- Outputting variables:
<%= title %> - Conditional logic:
<% if (user) { %> ... <% } else { %> ... <% } %> - Loops:
<% items.forEach(function(item) { %> ... <% }); %>
Using Includes (Partials) in EJS
EJS supports including partials to reuse template fragments:
File: views/partials/header.ejs
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1><%= title %></h1>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
</header>
File: views/partials/footer.ejs
<footer>
<p>© 2025 My Website</p>
</footer>
</body>
</html>
File: views/home.ejs
<%- include('partials/header') %>
<main>
<h2>Welcome to our website!</h2>
<p><%= message %></p>
<% if (featuredItems && featuredItems.length > 0) { %>
<h3>Featured Items:</h3>
<ul class="featured-list">
<% featuredItems.forEach(function(item) { %>
<li><%= item.name %> - $<%= item.price.toFixed(2) %></li>
<% }); %>
</ul>
<% } %>
</main>
<%- include('partials/footer') %>
Using includes makes it easy to maintain common elements like headers, footers, and navigation across multiple pages.
Passing Data to Partials
You can pass specific data to included partials:
File: views/partials/userCard.ejs
<div class="user-card">
<h3><%= user.name %></h3>
<p>Email: <%= user.email %></p>
<% if (user.role) { %>
<p>Role: <%= user.role %></p>
<% } %>
<% if (showActions) { %>
<div class="actions">
<a href="/users/<%= user.id %>/edit">Edit</a>
<a href="/users/<%= user.id %>/delete">Delete</a>
</div>
<% } %>
</div>
File: views/userList.ejs
<%- include('partials/header') %>
<h2>Users</h2>
<div class="user-list">
<% users.forEach(function(user) { %>
<%- include('partials/userCard', {
user: user,
showActions: currentUser.isAdmin
}) %>
<% }); %>
</div>
<%- include('partials/footer') %>
In this example, the userCard partial receives the current user object and a showActions flag that determines whether to display edit/delete links.
Pug (formerly Jade)
Pug is a high-performance template engine heavily influenced by Haml. It features a clean, whitespace-sensitive syntax that uses indentation to denote HTML element nesting.
Key Features
- Whitespace-sensitive syntax (no closing tags)
- Powerful template inheritance
- Mixins for reusable blocks
- Includes for partials
- Interpolation and unescaped interpolation
- Inline JavaScript
Basic Pug Template Example
doctype html
html
head
title= title
link(rel="stylesheet", href="/css/style.css")
body
header
h1= title
if user
p Welcome, #{user.name}!
else
p Please log in.
main
p= message
h2 Items:
ul
each item in items
li= item
footer
p © 2025 My Website
This template demonstrates basic Pug syntax:
- HTML elements without angle brackets or closing tags
- Element attributes in parentheses
- Variables with
=or#{variable}interpolation - Conditional blocks with
if/else - Iteration with
eachloops
Template Inheritance in Pug
Pug supports template inheritance, which is more powerful than simple includes:
File: views/layout.pug
doctype html
html
head
title #{title} - My Website
link(rel="stylesheet", href="/css/style.css")
block styles
body
header
h1 My Website
nav
ul
li: a(href="/") Home
li: a(href="/about") About
li: a(href="/contact") Contact
main
block content
footer
block footer
p © 2025 My Website
p
a(href="/privacy") Privacy Policy
| |
a(href="/terms") Terms of Service
script(src="/js/main.js")
block scripts
File: views/home.pug
extends layout
block styles
link(rel="stylesheet", href="/css/home.css")
block content
h2 Welcome to our website!
p= message
if featuredItems && featuredItems.length > 0
h3 Featured Items:
ul.featured-list
each item in featuredItems
li #{item.name} - $#{item.price.toFixed(2)}
.cta-section
h3 Get Started Today
p Join thousands of satisfied customers.
a.btn.btn-primary(href="/signup") Sign Up Now
block footer
p © 2025 My Website - Home of amazing products
p
a(href="/privacy") Privacy
| |
a(href="/terms") Terms
| |
a(href="/faq") FAQ
block scripts
script(src="/js/home.js")
In this example:
extends layoutinherits from the layout templateblock contentoverrides the content block defined in the layoutblock stylesandblock scriptsadd page-specific CSS and JavaScriptblock footerreplaces the default footer with page-specific content
Mixins in Pug
Mixins are reusable blocks of code, similar to functions:
File: views/mixins/userCard.pug
mixin userCard(user, showActions = false)
.user-card
h3= user.name
p Email: #{user.email}
if user.role
p Role: #{user.role}
if showActions
.actions
a(href=`/users/${user.id}/edit`) Edit
a(href=`/users/${user.id}/delete`) Delete
File: views/userList.pug
extends layout
include mixins/userCard
block content
h2 Users
.user-list
each user in users
+userCard(user, currentUser.isAdmin)
Mixins allow you to create reusable components with parameters, making your templates more modular and maintainable.
Handlebars
Handlebars is a popular "logic-less" templating engine that keeps the view and the code separated. It's based on the Mustache templating language.
Key Features
- Minimalist syntax with curly braces
- Logic-less templates (limited logic in templates)
- Built-in helpers for common operations
- Custom helpers for extending functionality
- Partials for reusable template fragments
- Block expressions
Basic Handlebars Template Example
<!DOCTYPE html>
<html>
<head>
<title>{{title}}</title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1>{{title}}</h1>
{{#if user}}
<p>Welcome, {{user.name}}!</p>
{{else}}
<p>Please log in.</p>
{{/if}}
</header>
<main>
<p>{{message}}</p>
<h2>Items:</h2>
<ul>
{{#each items}}
<li>{{this}}</li>
{{/each}}
</ul>
</main>
<footer>
<p>© 2025 My Website</p>
</footer>
</body>
</html>
This template shows basic Handlebars syntax:
- Variables with double curly braces:
{{title}} - Conditionals with
{{#if}}...{{else}}...{{/if}} - Loops with
{{#each}}...{{/each}}
Handlebars Layouts and Partials
With express-handlebars, you can use layouts and partials:
File: views/layouts/main.handlebars
<!DOCTYPE html>
<html>
<head>
<title>{{title}} - My Website</title>
<link rel="stylesheet" href="/css/style.css">
{{{block "styles"}}}
</head>
<body>
<header>
<h1>My Website</h1>
{{> partials/navigation}}
</header>
<main>
{{{body}}}
</main>
<footer>
{{> partials/footer}}
</footer>
<script src="/js/main.js"></script>
{{{block "scripts"}}}
</body>
</html>
File: views/partials/navigation.handlebars
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
File: views/partials/footer.handlebars
<p>© 2025 My Website</p>
<p>
<a href="/privacy">Privacy Policy</a> |
<a href="/terms">Terms of Service</a>
</p>
File: views/home.handlebars
{{#extend "styles"}}
<link rel="stylesheet" href="/css/home.css">
{{/extend}}
<h2>Welcome to our website!</h2>
<p>{{message}}</p>
{{#if featuredItems.length}}
<h3>Featured Items:</h3>
<ul class="featured-list">
{{#each featuredItems}}
<li>{{name}} - ${{formatPrice price}}</li>
{{/each}}
</ul>
{{/if}}
<div class="cta-section">
<h3>Get Started Today</h3>
<p>Join thousands of satisfied customers.</p>
<a href="/signup" class="btn btn-primary">Sign Up Now</a>
</div>
{{#extend "scripts"}}
<script src="/js/home.js"></script>
{{/extend}}
In this example:
- The main layout contains the basic HTML structure
- Partials (
{{> partials/navigation}}) include reusable components - The
{{{body}}}in the layout is replaced with the content of the rendered view - Block helpers allow extending specific sections of the layout
Handlebars Helpers
Helpers allow you to add custom functionality to your templates:
const { engine } = require('express-handlebars');
// Configure Handlebars with custom helpers
app.engine('handlebars', engine({
helpers: {
// Format price with 2 decimal places
formatPrice: function(price) {
return price.toFixed(2);
},
// Check if a value equals another value
eq: function(v1, v2) {
return v1 === v2;
},
// Get the current year
currentYear: function() {
return new Date().getFullYear();
},
// Format a date
formatDate: function(date, format) {
if (!date) return '';
const options = {};
if (format === 'short') {
options.year = 'numeric';
options.month = 'short';
options.day = 'numeric';
} else if (format === 'long') {
options.weekday = 'long';
options.year = 'numeric';
options.month = 'long';
options.day = 'numeric';
}
return new Date(date).toLocaleDateString('en-US', options);
}
}
}));
// Usage in a template:
// {{formatPrice product.price}}
// {{#if (eq status "active")}}Active!{{/if}}
// © {{currentYear}} My Company
// Posted on: {{formatDate post.createdAt "long"}}
Helpers encapsulate logic that would otherwise clutter your templates, keeping them focused on presentation rather than computation.
Dynamic Data and Context
The power of template engines comes from their ability to use dynamic data to render different HTML based on the application state.
Passing Data to Templates
Data is passed to templates when calling res.render():
app.get('/profile/:username', async (req, res) => {
try {
// Fetch user data from database
const user = await User.findOne({ username: req.params.username });
if (!user) {
return res.status(404).render('error', {
title: 'User Not Found',
message: `No user found with username: ${req.params.username}`
});
}
// Fetch user's posts
const posts = await Post.find({ userId: user._id }).sort({ createdAt: -1 }).limit(5);
// Render profile page with user data
res.render('profile', {
title: `${user.displayName}'s Profile`,
user: {
username: user.username,
displayName: user.displayName,
bio: user.bio,
avatarUrl: user.avatarUrl,
joinDate: user.createdAt
},
posts: posts.map(post => ({
id: post._id,
title: post.title,
summary: post.content.substring(0, 100) + '...',
createdAt: post.createdAt
})),
isOwner: req.user && req.user.id === user._id.toString()
});
} catch (err) {
console.error('Error fetching profile:', err);
res.status(500).render('error', {
title: 'Server Error',
message: 'An error occurred while fetching the profile'
});
}
});
In this example, we:
- Fetch user and post data from the database
- Prepare the data for the template (formatting, filtering sensitive information)
- Add contextual data like
isOwnerto control what's displayed - Render different templates based on the request outcome (profile or error)
Real-world Example: E-commerce Product Page
Let's look at how a product page might be implemented in an e-commerce application:
Route Handler (Express):
app.get('/products/:slug', async (req, res) => {
try {
// Fetch product data
const product = await Product.findOne({ slug: req.params.slug })
.populate('category', 'name slug')
.populate('reviews.userId', 'username avatarUrl');
if (!product) {
return res.status(404).render('error', {
title: 'Product Not Found',
message: 'The requested product does not exist'
});
}
// Fetch related products
const relatedProducts = await Product.find({
category: product.category._id,
_id: { $ne: product._id } // Exclude current product
})
.limit(4)
.select('name slug price imageUrl');
// Check if product is in user's cart or wishlist
let inCart = false;
let inWishlist = false;
if (req.user) {
const cart = await Cart.findOne({ userId: req.user.id });
if (cart) {
inCart = cart.items.some(item => item.productId.toString() === product._id.toString());
}
const wishlist = await Wishlist.findOne({ userId: req.user.id });
if (wishlist) {
inWishlist = wishlist.items.some(itemId => itemId.toString() === product._id.toString());
}
}
// Calculate average rating
let avgRating = 0;
if (product.reviews.length > 0) {
const totalRating = product.reviews.reduce((sum, review) => sum + review.rating, 0);
avgRating = totalRating / product.reviews.length;
}
// Check stock status
const stockStatus = product.stockQuantity > 0
? (product.stockQuantity < 5 ? 'low' : 'in_stock')
: 'out_of_stock';
// Format data for the template
res.render('product', {
title: product.name,
product: {
id: product._id,
name: product.name,
description: product.description,
price: product.price,
salePrice: product.salePrice,
images: product.images,
category: product.category,
specs: product.specifications,
stockStatus,
stockQuantity: product.stockQuantity
},
reviews: product.reviews.map(review => ({
user: {
username: review.userId.username,
avatar: review.userId.avatarUrl
},
rating: review.rating,
title: review.title,
content: review.content,
date: review.createdAt
})),
relatedProducts: relatedProducts.map(p => ({
id: p._id,
name: p.name,
slug: p.slug,
price: p.price,
image: p.imageUrl
})),
meta: {
avgRating,
reviewCount: product.reviews.length
},
userContext: {
inCart,
inWishlist,
isLoggedIn: !!req.user
}
});
} catch (err) {
console.error('Error fetching product:', err);
res.status(500).render('error', {
title: 'Server Error',
message: 'An error occurred while fetching the product'
});
}
});
EJS Template (views/product.ejs):
<%- include('partials/header', { title: title }) %>
<div class="product-page">
<div class="breadcrumbs">
<a href="/">Home</a> >
<a href="/categories/<%= product.category.slug %>"><%= product.category.name %></a> >
<span><%= product.name %></span>
</div>
<div class="product-container">
<div class="product-gallery">
<div class="main-image">
<img src="<%= product.images[0] %>" alt="<%= product.name %>">
</div>
<% if (product.images.length > 1) { %>
<div class="thumbnail-gallery">
<% product.images.forEach((image, index) => { %>
<div class="thumbnail <%= index === 0 ? 'active' : '' %>" data-image-index="<%= index %>">
<img src="<%= image %>" alt="<%= product.name %> thumbnail">
</div>
<% }); %>
</div>
<% } %>
</div>
<div class="product-info">
<h1><%= product.name %></h1>
<div class="product-meta">
<div class="ratings">
<div class="stars" data-rating="<%= meta.avgRating %>">
<% for(let i = 1; i <= 5; i++) { %>
<span class="star <%= i <= Math.round(meta.avgRating) ? 'filled' : '' %>">★</span>
<% } %>
</div>
<span class="review-count"><%= meta.reviewCount %> reviews</span>
</div>
</div>
<div class="pricing">
<% if (product.salePrice && product.salePrice < product.price) { %>
<span class="original-price">$<%= product.price.toFixed(2) %></span>
<span class="sale-price">$<%= product.salePrice.toFixed(2) %></span>
<span class="discount-badge">
Save <%= Math.round((1 - product.salePrice / product.price) * 100) %>%
</span>
<% } else { %>
<span class="regular-price">$<%= product.price.toFixed(2) %></span>
<% } %>
</div>
<div class="stock-status <%= product.stockStatus %>">
<% if (product.stockStatus === 'in_stock') { %>
<span>In Stock (<%= product.stockQuantity %> available)</span>
<% } else if (product.stockStatus === 'low') { %>
<span>Only <%= product.stockQuantity %> left in stock - order soon!</span>
<% } else { %>
<span>Out of Stock</span>
<% } %>
</div>
<div class="product-description">
<p><%= product.description %></p>
</div>
<div class="product-actions">
<form action="/cart/add" method="POST">
<input type="hidden" name="productId" value="<%= product.id %>">
<div class="quantity-selector">
<label for="quantity">Quantity:</label>
<select name="quantity" id="quantity">
<% for(let i = 1; i <= Math.min(10, product.stockQuantity); i++) { %>
<option value="<%= i %>"><%= i %></option>
<% } %>
</select>
</div>
<div class="buttons">
<button
type="submit"
class="btn-add-to-cart"
<%= product.stockStatus === 'out_of_stock' ? 'disabled' : '' %>
>
<% if (userContext.inCart) { %>
Update Cart
<% } else { %>
Add to Cart
<% } %>
</button>
<% if (userContext.isLoggedIn) { %>
<button
type="button"
class="btn-wishlist <%= userContext.inWishlist ? 'in-wishlist' : '' %>"
data-product-id="<%= product.id %>"
>
<i class="icon-heart"></i>
<% if (userContext.inWishlist) { %>
Remove from Wishlist
<% } else { %>
Add to Wishlist
<% } %>
</button>
<% } %>
</div>
</form>
</div>
</div>
</div>
<div class="product-tabs">
<div class="tab-headers">
<button class="tab-header active" data-tab="specifications">Specifications</button>
<button class="tab-header" data-tab="reviews">Reviews (<%= meta.reviewCount %>)</button>
</div>
<div class="tab-content">
<div class="tab-panel active" id="specifications">
<table class="specs-table">
<% Object.entries(product.specs).forEach(([key, value]) => { %>
<tr>
<th><%= key %></th>
<td><%= value %></td>
</tr>
<% }); %>
</table>
</div>
<div class="tab-panel" id="reviews">
<% if (reviews.length > 0) { %>
<div class="reviews-list">
<% reviews.forEach(review => { %>
<div class="review">
<div class="review-header">
<div class="reviewer">
<img src="<%= review.user.avatar %>" alt="<%= review.user.username %>">
<span><%= review.user.username %></span>
</div>
<div class="review-rating">
<% for(let i = 1; i <= 5; i++) { %>
<span class="star <%= i <= review.rating ? 'filled' : '' %>">★</span>
<% } %>
</div>
<div class="review-date">
<%= new Date(review.date).toLocaleDateString() %>
</div>
</div>
<h4 class="review-title"><%= review.title %></h4>
<p class="review-content"><%= review.content %></p>
</div>
<% }); %>
</div>
<% } else { %>
<p class="no-reviews">This product has no reviews yet. Be the first to review it!</p>
<% } %>
<% if (userContext.isLoggedIn) { %>
<button class="btn-write-review">Write a Review</button>
<div class="review-form-container hidden">
<form action="/products/<%= product.id %>/reviews" method="POST">
<h3>Write Your Review</h3>
<div class="form-group">
<label for="rating">Rating:</label>
<div class="rating-input">
<% for(let i = 5; i >= 1; i--) { %>
<input type="radio" name="rating" id="rating-<%= i %>" value="<%= i %>">
<label for="rating-<%= i %>">★</label>
<% } %>
</div>
</div>
<div class="form-group">
<label for="review-title">Title:</label>
<input type="text" id="review-title" name="title" required>
</div>
<div class="form-group">
<label for="review-content">Review:</label>
<textarea id="review-content" name="content" rows="5" required></textarea>
</div>
<button type="submit" class="btn-submit-review">Submit Review</button>
</form>
</div>
<% } else { %>
<p class="login-to-review">
<a href="/login?redirect=/products/<%= product.slug %>">Log in</a> to write a review.
</p>
<% } %>
</div>
</div>
</div>
<% if (relatedProducts.length > 0) { %>
<div class="related-products">
<h2>You may also like</h2>
<div class="products-grid">
<% relatedProducts.forEach(product => { %>
<div class="product-card">
<a href="/products/<%= product.slug %>">
<div class="product-image">
<img src="<%= product.image %>" alt="<%= product.name %>">
</div>
<div class="product-details">
<h3><%= product.name %></h3>
<p class="product-price">$<%= product.price.toFixed(2) %></p>
</div>
</a>
<button class="quick-add" data-product-id="<%= product.id %>">Quick Add</button>
</div>
<% }); %>
</div>
</div>
<% } %>
</div>
<script src="/js/product.js"></script>
<%- include('partials/footer') %>
This example demonstrates how real-world applications use templates to:
- Show/hide elements based on data (sale prices, stock status)
- Format and transform data (price formatting, date formatting)
- Display dynamic user context (in cart, in wishlist)
- Create conditional interfaces (review form shown only to logged-in users)
- Generate repetitive UI elements (reviews, related products)
Template Engine Performance Considerations
Template rendering can impact the performance of your application. Here are some best practices to optimize template rendering:
Caching
Many template engines support caching compiled templates to improve performance:
// Enable template caching in production
app.set('view cache', process.env.NODE_ENV === 'production');
Some engines like EJS also support client-side caching:
const ejs = require('ejs');
// Compile a template once and cache it
const renderTemplate = ejs.compile(
fs.readFileSync('./views/template.ejs', 'utf8'),
{ filename: './views/template.ejs' }
);
// Use the cached template multiple times
app.get('/cached-template', (req, res) => {
const html = renderTemplate({
title: 'Cached Template',
data: { /* ... */ }
});
res.send(html);
});
Partial Rendering
For dynamic updates, consider rendering only the parts that change:
app.get('/api/refresh-content', (req, res) => {
// Fetch new data
const newData = { /* ... */ };
// Render only a partial template
res.render('partials/content-section', newData, (err, html) => {
if (err) return res.status(500).json({ error: err.message });
res.json({ html });
});
});
The front-end can then update just that section of the page using AJAX.
Minimize Template Logic
Keep templates focused on presentation, and move complex logic to the controller or service layer:
// Instead of this (complex logic in template):
{{#each products}}
{{#if (and (gt price 50) (lt price 100) (eq category "electronics"))}}
<div class="mid-range-electronics">...</div>
{{/if}}
{{/each}}
// Do this (preprocess data in controller):
const midRangeElectronics = products.filter(p =>
p.price > 50 && p.price < 100 && p.category === 'electronics'
);
res.render('products', { midRangeElectronics });
// Then in template:
{{#each midRangeElectronics}}
<div class="mid-range-electronics">...</div>
{{/each}}
This approach makes templates cleaner, faster to render, and easier to maintain.
Beyond Templates: Modern Approaches
While traditional server-side templates are still valuable, modern web development often combines them with other approaches:
API + Client-side Rendering
Many applications use Express as an API server and render the UI on the client with frameworks like React or Vue:
// Express API endpoint
app.get('/api/products', async (req, res) => {
try {
const products = await Product.find();
res.json(products);
} catch (err) {
res.status(500).json({ error: err.message });
}
});
// Client-side React component
function ProductList() {
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/products')
.then(res => res.json())
.then(data => {
setProducts(data);
setLoading(false);
})
.catch(err => {
console.error('Error fetching products:', err);
setLoading(false);
});
}, []);
if (loading) return <div>Loading...</div>;
return (
<div className="products-grid">
{products.map(product => (
<ProductCard key={product._id} product={product} />
))}
</div>
);
}
Server-side Rendering with React/Vue
Frameworks like Next.js and Nuxt.js combine server-side rendering with client-side hydration:
// Next.js page with SSR
export async function getServerSideProps() {
const res = await fetch('http://api.example.com/products');
const products = await res.json();
return {
props: { products }
};
}
function ProductPage({ products }) {
return (
<div>
<h1>Products</h1>
<div className="products-grid">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
);
}
<script src="/js/dashboard.js"></script>
This approach allows you to leverage the benefits of both server-side and client-side rendering:
- Initial page load is fast and SEO-friendly (server-rendered)
- Subsequent interactions are smooth and responsive (client-side enhanced)
- Progressive enhancement ensures functionality without JavaScript
- Real-time updates can be added without changing the core structure
Hybrid Approaches
Some applications use traditional templates for the main layout and client-side JavaScript for interactive components:
// Express route that renders a template
app.get('/dashboard', authenticate, (req, res) => {
res.render('dashboard', {
title: 'Dashboard',
user: req.user,
initialData: {
// Data needed for initial render
stats: { /* ... */ },
recentActivity: { /* ... */ }
}
});
});
// In the dashboard.ejs template:
<script>
// Make initialData available to client-side code
window.INITIAL_DATA = <%- JSON.stringify(initialData) %>;
</script>
<div id="chart-container" data-endpoint="/api/chart-data"></div>
<div id="activity-feed" data-endpoint="/api/activity">
<!-- Initial server-rendered content -->
<% recentActivity.forEach(item => { %>
<div class="activity-item">
<!-- Item content -->
</div>
<% }); %>
&
Beyond Templates: Modern Approaches (Continued)
Hybrid Approaches
Some applications use traditional templates for the main layout and client-side JavaScript for interactive components:
// Express route that renders a template
app.get('/dashboard', authenticate, (req, res) => {
res.render('dashboard', {
title: 'Dashboard',
user: req.user,
initialData: {
// Data needed for initial render
stats: { /* ... */ },
recentActivity: { /* ... */ }
}
});
});
// In the dashboard.ejs template:
<script>
// Make initialData available to client-side code
window.INITIAL_DATA = <%- JSON.stringify(initialData) %>;
</script>
<div id="chart-container" data-endpoint="/api/chart-data"></div>
<div id="activity-feed" data-endpoint="/api/activity">
<!-- Initial server-rendered content -->
<% recentActivity.forEach(item => { %>
<div class="activity-item">
<!-- Item content -->
</div>
<% }); %>
</div>
<script src="/js/dashboard.js"></script>
Then, the client-side JavaScript enhances the server-rendered content:
// dashboard.js
document.addEventListener('DOMContentLoaded', () => {
// Initialize with server-provided data
const { stats } = window.INITIAL_DATA;
// Render chart using server data
renderChart(document.getElementById('chart-container'), stats);
// Set up real-time updates for activity feed
const activityFeed = document.getElementById('activity-feed');
const endpoint = activityFeed.dataset.endpoint;
// Poll for updates every 30 seconds
setInterval(async () => {
const response = await fetch(endpoint);
const newActivity = await response.json();
updateActivityFeed(activityFeed, newActivity);
}, 30000);
});
When to Use Each Approach
Approach
Best For
Example Use Cases
Traditional Server Templates
- Content-focused sites
- SEO-critical pages
- Simple interfaces
- Progressive enhancement
- Blog platforms
- Documentation sites
- E-commerce product listings
- Content management systems
API + Client Rendering
- Highly interactive UIs
- Real-time applications
- Complex client-side state
- Single Page Apps (SPAs)
- Admin dashboards
- Social media apps
- Real-time collaboration tools
- Interactive data visualizations
SSR with React/Vue
- Complex UIs with SEO needs
- Performance-critical applications
- Progressive web apps
- E-commerce platforms
- News sites
- Marketing websites
- Content portals
Hybrid Approaches
- Gradually evolving applications
- Mixed content/interactive needs
- Applications with varying page types
- Enterprise applications
- E-learning platforms
- Community forums
- Marketplace platforms
Building a Complete Express Application with Templates
Let's walk through building a simple Express application with templates to demonstrate a practical implementation.
graph TD
A[Express App] --> B[Route Handlers]
A --> C[View Templates]
A --> D[Static Assets]
B --> E[Model/Data Layer]
E --> B
B --> C
C --> F[Page HTML]
style A fill:#f9d71c,stroke:#333,stroke-width:2px
style C fill:#a1ffa8,stroke:#333,stroke-width:2px
Project Setup
// Install required packages
npm init -y
npm install express ejs morgan
// Create project structure
mkdir -p public/{css,js,img} views/{layouts,partials} routes controllers models
touch app.js
Express Application Setup (app.js)
const express = require('express');
const path = require('path');
const morgan = require('morgan');
// Import routes
const indexRoutes = require('./routes/index');
const productRoutes = require('./routes/products');
const app = express();
// View engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// Middleware
app.use(morgan('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static(path.join(__dirname, 'public')));
// Custom middleware to add global template data
app.use((req, res, next) => {
// Add current year for copyright in footer
res.locals.currentYear = new Date().getFullYear();
// Add site name for all templates
res.locals.siteName = 'Express Shop';
// Add current path for navigation highlighting
res.locals.currentPath = req.path;
next();
});
// Routes
app.use('/', indexRoutes);
app.use('/products', productRoutes);
// 404 handler
app.use((req, res, next) => {
res.status(404).render('error', {
title: 'Page Not Found',
message: 'The page you requested could not be found.'
});
});
// Error handler
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).render('error', {
title: 'Error',
message: process.env.NODE_ENV === 'development'
? err.message
: 'An unexpected error occurred.'
});
});
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
module.exports = app;
Route Handlers (routes/products.js)
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');
// Product routes
router.get('/', productController.getAllProducts);
router.get('/category/:category', productController.getProductsByCategory);
router.get('/:id', productController.getProductById);
module.exports = router;
Controllers (controllers/productController.js)
const Product = require('../models/product');
// Mock data (in a real app, this would come from a database)
const products = [
{
id: '1',
name: 'Wireless Headphones',
description: 'Premium wireless headphones with noise cancellation.',
price: 199.99,
image: '/img/headphones.jpg',
category: 'electronics'
},
{
id: '2',
name: 'Fitness Tracker',
description: 'Track your steps, heart rate, and sleep patterns.',
price: 89.99,
image: '/img/fitness-tracker.jpg',
category: 'electronics'
},
{
id: '3',
name: 'Cotton T-Shirt',
description: 'Comfortable cotton t-shirt for everyday wear.',
price: 19.99,
image: '/img/tshirt.jpg',
category: 'clothing'
}
];
// Get all products
exports.getAllProducts = (req, res) => {
res.render('products/index', {
title: 'All Products',
products: products
});
};
// Get products by category
exports.getProductsByCategory = (req, res) => {
const category = req.params.category;
const filteredProducts = products.filter(p => p.category === category);
res.render('products/index', {
title: `${category.charAt(0).toUpperCase() + category.slice(1)} Products`,
products: filteredProducts,
category: category
});
};
// Get product by ID
exports.getProductById = (req, res, next) => {
const id = req.params.id;
const product = products.find(p => p.id === id);
if (!product) {
return next(new Error('Product not found'));
}
// Get related products (same category but different ID)
const relatedProducts = products
.filter(p => p.category === product.category && p.id !== id)
.slice(0, 3);
res.render('products/detail', {
title: product.name,
product: product,
relatedProducts: relatedProducts
});
};
View Templates
Layout Template (views/partials/layout.ejs)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= title %> - <%= siteName %></title>
<link rel="stylesheet" href="/css/styles.css">
<% if (typeof extraStyles !== 'undefined') { %>
<% extraStyles.forEach(style => { %>
<link rel="stylesheet" href="<%= style %>">
<% }); %>
<% } %>
</head>
<body>
<header>
<%- include('header') %>
</header>
<main>
<% if (typeof breadcrumbs !== 'undefined') { %>
<div class="breadcrumbs">
<%- include('breadcrumbs', { breadcrumbs }) %>
</div>
<% } %>
<%- body %>
</main>
<footer>
<%- include('footer') %>
</footer>
<script src="/js/main.js"></script>
<% if (typeof extraScripts !== 'undefined') { %>
<% extraScripts.forEach(script => { %>
<script src="<%= script %>"></script>
<% }); %>
<% } %>
</body>
</html>
Header Partial (views/partials/header.ejs)
<div class="header-container">
<div class="logo">
<a href="/"><%= siteName %></a>
</div>
<nav class="main-nav">
<ul>
<li><a href="/" class="<%= currentPath === '/' ? 'active' : '' %>">Home</a></li>
<li><a href="/products" class="<%= currentPath.startsWith('/products') ? 'active' : '' %>">Products</a></li>
<li><a href="/about" class="<%= currentPath === '/about' ? 'active' : '' %>">About</a></li>
<li><a href="/contact" class="<%= currentPath === '/contact' ? 'active' : '' %>">Contact</a></li>
</ul>
</nav>
</div>
Footer Partial (views/partials/footer.ejs)
<div class="footer-container">
<div class="footer-links">
<div class="footer-section">
<h3>Shop</h3>
<ul>
<li><a href="/products">All Products</a></li>
<li><a href="/products/category/electronics">Electronics</a></li>
<li><a href="/products/category/clothing">Clothing</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Company</h3>
<ul>
<li><a href="/about">About Us</a></li>
<li><a href="/contact">Contact</a></li>
<li><a href="/careers">Careers</a></li>
</ul>
</div>
<div class="footer-section">
<h3>Support</h3>
<ul>
<li><a href="/help">Help Center</a></li>
<li><a href="/returns">Returns & Refunds</a></li>
<li><a href="/privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
<div class="footer-bottom">
<p>© <%= currentYear %> <%= siteName %>. All rights reserved.</p>
</div>
</div>
Product List Template (views/products/index.ejs)
<% const breadcrumbs = [
{ name: 'Home', url: '/' },
{ name: 'Products', url: '/products' }
];
if (typeof category !== 'undefined') {
breadcrumbs.push({
name: category.charAt(0).toUpperCase() + category.slice(1),
url: `/products/category/${category}`
});
}
%>
<%- include('../partials/layout', {
title: title,
breadcrumbs: breadcrumbs,
body: `
<div class="products-container">
<h1>${title}</h1>
<div class="product-filters">
<div class="filter-group">
<h3>Categories</h3>
<ul>
<li>
<a href="/products" class="${typeof category === 'undefined' ? 'active' : ''}">
All Categories
</a>
</li>
<li>
<a href="/products/category/electronics" class="${category === 'electronics' ? 'active' : ''}">
Electronics
</a>
</li>
<li>
<a href="/products/category/clothing" class="${category === 'clothing' ? 'active' : ''}">
Clothing
</a>
</li>
</ul>
</div>
</div>
<div class="products-grid">
${products.length > 0 ? products.map(product => `
<div class="product-card">
<a href="/products/${product.id}">
<div class="product-image">
<img src="${product.image}" alt="${product.name}">
</div>
<div class="product-details">
<h3>${product.name}</h3>
<p class="product-price">$${product.price.toFixed(2)}</p>
<p class="product-description">${product.description.substring(0, 70)}${product.description.length > 70 ? '...' : ''}</p>
</div>
</a>
<button class="add-to-cart" data-product-id="${product.id}">Add to Cart</button>
</div>
`).join('') : `
<div class="no-products">
<p>No products found.</p>
</div>
`}
</div>
</div>
`,
extraScripts: ['/js/products.js']
}) %>
Product Detail Template (views/products/detail.ejs)
<% const breadcrumbs = [
{ name: 'Home', url: '/' },
{ name: 'Products', url: '/products' },
{ name: product.name, url: `/products/${product.id}` }
]; %>
<%- include('../partials/layout', {
title: product.name,
breadcrumbs: breadcrumbs,
body: `
<div class="product-detail">
<div class="product-image-container">
<img src="${product.image}" alt="${product.name}" class="product-image">
</div>
<div class="product-info">
<h1>${product.name}</h1>
<p class="product-price">$${product.price.toFixed(2)}</p>
<div class="product-description">
<p>${product.description}</p>
</div>
<div class="product-actions">
<div class="quantity-selector">
<label for="quantity">Quantity:</label>
<select id="quantity" name="quantity">
${[1, 2, 3, 4, 5].map(num => `
<option value="${num}">${num}</option>
`).join('')}
</select>
</div>
<button id="add-to-cart" class="btn-primary" data-product-id="${product.id}">
Add to Cart
</button>
<button id="add-to-wishlist" class="btn-secondary" data-product-id="${product.id}">
Add to Wishlist
</button>
</div>
</div>
</div>
${relatedProducts.length > 0 ? `
<div class="related-products">
<h2>You May Also Like</h2>
<div class="products-row">
${relatedProducts.map(product => `
<div class="product-card">
<a href="/products/${product.id}">
<div class="product-image">
<img src="${product.image}" alt="${product.name}">
</div>
<div class="product-details">
<h3>${product.name}</h3>
<p class="product-price">$${product.price.toFixed(2)}</p>
</div>
</a>
<button class="add-to-cart" data-product-id="${product.id}">Add to Cart</button>
</div>
`).join('')}
</div>
</div>
` : ''}
`,
extraScripts: ['/js/product-detail.js']
}) %>
Error Template (views/error.ejs)
<%- include('partials/layout', {
title: title,
body: `
<div class="error-container">
<h1>${title}</h1>
<p>${message}</p>
<a href="/" class="btn-primary">Return to Homepage</a>
</div>
`
}) %>
Client-side JavaScript for Interactivity
// public/js/product-detail.js
document.addEventListener('DOMContentLoaded', () => {
const addToCartBtn = document.getElementById('add-to-cart');
const addToWishlistBtn = document.getElementById('add-to-wishlist');
const quantitySelect = document.getElementById('quantity');
addToCartBtn.addEventListener('click', () => {
const productId = addToCartBtn.dataset.productId;
const quantity = parseInt(quantitySelect.value);
// In a real app, this would send an AJAX request to add the item to cart
console.log(`Adding product ${productId} to cart, quantity: ${quantity}`);
// Show confirmation message
const message = document.createElement('div');
message.className = 'message success';
message.textContent = 'Product added to cart!';
document.querySelector('.product-actions').appendChild(message);
// Remove message after 3 seconds
setTimeout(() => {
message.remove();
}, 3000);
});
addToWishlistBtn.addEventListener('click', () => {
const productId = addToWishlistBtn.dataset.productId;
// In a real app, this would send an AJAX request to add the item to wishlist
console.log(`Adding product ${productId} to wishlist`);
// Toggle button state
addToWishlistBtn.classList.toggle('in-wishlist');
if (addToWishlistBtn.classList.contains('in-wishlist')) {
addToWishlistBtn.textContent = 'Remove from Wishlist';
} else {
addToWishlistBtn.textContent = 'Add to Wishlist';
}
});
});
Practical Exercises
Practice using different template engines with Express in these exercises:
Exercise 1: Basic EJS Template
Objective: Create a simple Express application using EJS templates to display a list of products.
Tasks:
- Set up an Express project with EJS as the template engine
- Create a mock product database (JavaScript array)
- Create routes to display all products and individual product details
- Create templates for:
- A layout/header/footer
- Product list page
- Product detail page
- Add basic styling to make the pages look presentable
Exercise 2: Blog with Pug Templates
Objective: Build a simple blog application using Pug templates and template inheritance.
Tasks:
- Create an Express application using Pug as the template engine
- Create a mock blog post database with post title, content, author, and date
- Implement these routes:
- Home page - shows recent posts
- Post detail page - shows a single post
- About page - static page about the blog
- Create templates using Pug's inheritance features:
- A base layout
- A posts list template
- A post detail template
- An about page template
- Create mixins for reusable components like post previews
Exercise 3: Handlebars Dashboard
Objective: Create a dashboard application using Handlebars templates and custom helpers.
Tasks:
- Set up an Express application with express-handlebars
- Configure Handlebars with layouts, partials, and custom helpers
- Create mock data for:
- User information
- Recent activity
- Statistics/metrics
- Create templates for a dashboard that includes:
- User profile section
- Activity feed
- Stats/metrics cards
- Navigation menu
- Add custom Handlebars helpers for formatting dates, numbers, etc.
Exercise 4: Multi-engine Comparison
Objective: Compare different template engines by implementing the same page with each.
Tasks:
- Create three Express applications (or one app with three different routes) using:
- EJS
- Pug
- Handlebars
- For each template engine, implement the same product detail page
- Ensure all implementations include:
- Data passing from controller to template
- Conditional rendering
- Loops/iteration
- Includes/partials
- Compare the syntax, readability, and maintainability of each implementation
- Create a simple write-up of your findings and preferences
Further Resources