Template Engines in Express

Module 22: Web Frameworks I (JavaScript) - Monday: Express.js Fundamentals

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.

graph TD A[Template File] --> B{Template Engine} C[Dynamic Data] --> B B --> D[Generated HTML] D --> E[Client Browser] style B fill:#f9d71c,stroke:#333,stroke-width:2px

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

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
  • Full JavaScript support
  • Partials/includes
  • Custom delimiters
  • Familiar for those who know HTML + JS
  • Minimal learning curve
  • Flexible
Applications where developers need to use JavaScript logic directly in templates
Pug (formerly Jade) Indentation-based minimalist syntax
  • Inheritance
  • Mixins
  • Interpolation
  • Conditionals and loops
  • Clean, concise syntax
  • Powerful layout system
  • Fewer typing errors
Complex layouts with nested structures and reusable components
Handlebars Logicless templates with curly braces
  • Partials
  • Helpers
  • Block expressions
  • Strict separation of logic and presentation
  • Simple syntax
  • Extensible with helpers
Projects with non-technical template editors or strict MVC separation
Nunjucks Jinja2-inspired syntax
  • Inheritance
  • Asynchronous control
  • Macros
  • Custom filters
  • Powerful yet readable
  • Rich feature set
  • Good for complex templates
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 use
  • app.set('views', './views') specifies the directory where template files are located
  • res.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

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

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 each loops

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 layout inherits from the layout template
  • block content overrides the content block defined in the layout
  • block styles and block scripts add page-specific CSS and JavaScript
  • block footer replaces 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

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:

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:

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:

  1. Set up an Express project with EJS as the template engine
  2. Create a mock product database (JavaScript array)
  3. Create routes to display all products and individual product details
  4. Create templates for:
    • A layout/header/footer
    • Product list page
    • Product detail page
  5. 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:

  1. Create an Express application using Pug as the template engine
  2. Create a mock blog post database with post title, content, author, and date
  3. Implement these routes:
    • Home page - shows recent posts
    • Post detail page - shows a single post
    • About page - static page about the blog
  4. Create templates using Pug's inheritance features:
    • A base layout
    • A posts list template
    • A post detail template
    • An about page template
  5. Create mixins for reusable components like post previews

Exercise 3: Handlebars Dashboard

Objective: Create a dashboard application using Handlebars templates and custom helpers.

Tasks:

  1. Set up an Express application with express-handlebars
  2. Configure Handlebars with layouts, partials, and custom helpers
  3. Create mock data for:
    • User information
    • Recent activity
    • Statistics/metrics
  4. Create templates for a dashboard that includes:
    • User profile section
    • Activity feed
    • Stats/metrics cards
    • Navigation menu
  5. 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:

  1. Create three Express applications (or one app with three different routes) using:
    • EJS
    • Pug
    • Handlebars
  2. For each template engine, implement the same product detail page
  3. Ensure all implementations include:
    • Data passing from controller to template
    • Conditional rendering
    • Loops/iteration
    • Includes/partials
  4. Compare the syntax, readability, and maintainability of each implementation
  5. Create a simple write-up of your findings and preferences

Further Resources