Skip to main content

Express Template Best Practices

When building web applications with Express.js, templating engines play a crucial role in generating dynamic HTML content. Following best practices ensures that your templates remain maintainable, secure, and efficient. This guide will walk you through essential best practices for working with Express templates, no matter which templating engine you use.

Introduction

Templates in Express.js allow you to render dynamic content on your web pages. Whether you're using EJS, Pug (formerly Jade), Handlebars, or another templating engine, adopting good practices from the start will save you time and prevent common issues as your application grows.

These best practices focus on:

  • Code organization
  • Security considerations
  • Performance optimization
  • Maintainability
  • Common pitfalls to avoid

Choosing the Right Templating Engine

Before diving into best practices, it's important to choose the right templating engine for your needs.

Here's a comparison of popular templating engines used with Express:

EngineSyntaxLearning CurveFeatures
EJSHTML with <% %> tagsLowSimple, similar to PHP/ASP
PugIndentation-based, minimalMediumConcise, powerful features
HandlebarsHTML with {{}} expressionsLowLogic-less templates

Choose based on your team's familiarity and project requirements.

Directory Structure Best Practices

A well-organized directory structure makes your application easier to maintain.

project-root/
├── views/
│ ├── layouts/
│ │ ├── main.ejs
│ │ └── admin.ejs
│ ├── partials/
│ │ ├── header.ejs
│ │ ├── footer.ejs
│ │ └── navigation.ejs
│ └── pages/
│ ├── home.ejs
│ ├── about.ejs
│ └── contact.ejs
├── public/
│ ├── css/
│ ├── js/
│ └── images/
└── app.js

Setting Up Your Views

In your Express application:

javascript
const express = require('express');
const path = require('path');
const app = express();

// Set the view engine
app.set('view engine', 'ejs'); // Or 'pug', 'handlebars', etc.

// Set the views directory
app.set('views', path.join(__dirname, 'views'));

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

Template Partials and Layouts

Using Partials

Partials allow you to reuse components across multiple pages.

EJS Example:

javascript
// In your Express route
app.get('/', (req, res) => {
res.render('pages/home', {
title: 'Home Page',
user: { name: 'John' }
});
});
html
<!-- views/pages/home.ejs -->
<%- include('../partials/header', {title: title}) %>

<h1>Welcome, <%= user.name %>!</h1>
<p>This is the home page content.</p>

<%- include('../partials/footer') %>
html
<!-- views/partials/header.ejs -->
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<%- include('./navigation') %>

Using Layouts

Layouts provide a template that wraps around each page's content.

Using express-ejs-layouts:

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

// Use layouts
app.use(expressLayouts);
app.set('layout', 'layouts/main');
html
<!-- views/layouts/main.ejs -->
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel="stylesheet" href="/css/style.css">
<%- style %>
</head>
<body>
<%- include('../partials/navigation') %>

<main>
<%- body %>
</main>

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

Data Passing Best Practices

Use Local Variables Consistently

javascript
// Consistent approach for all routes
app.get('/product/:id', (req, res) => {
const productId = req.params.id;

// Get product data from database
const product = getProductById(productId);

res.render('pages/product', {
title: product.name,
product: product,
categories: getAllCategories(),
user: req.user,
isAuthenticated: req.isAuthenticated()
});
});

Use Middleware for Common Variables

javascript
// Middleware to add common variables to all templates
app.use((req, res, next) => {
res.locals.user = req.user;
res.locals.isAuthenticated = req.isAuthenticated();
res.locals.siteTitle = "My Awesome Website";
next();
});

Security Best Practices

Prevent XSS Attacks

Most templating engines escape content by default, but it's important to understand when content is escaped vs. unescaped:

EJS:

  • <%= content %> - Escaped output
  • <%- content %> - Unescaped output (only use for trusted HTML)

Pug:

  • #{content} or p= content - Escaped output
  • !{content} or p!= content - Unescaped output (only use for trusted HTML)

Handlebars:

  • {{content}} - Escaped output
  • {{{content}}} - Unescaped output (only use for trusted HTML)

Example of Safe vs. Unsafe Output

javascript
app.get('/profile', (req, res) => {
const userData = {
name: "John Doe",
bio: "<script>alert('XSS attack!');</script>This is my bio"
};

res.render('pages/profile', { user: userData });
});
html
<!-- Safe (EJS) -->
<h1>Welcome, <%= user.name %></h1>
<div class="bio"><%= user.bio %></div>

<!-- Output: -->
<!-- <h1>Welcome, John Doe</h1> -->
<!-- <div class="bio">&lt;script&gt;alert('XSS attack!');&lt;/script&gt;This is my bio</div> -->

<!-- Unsafe (EJS) - Only use for trusted content -->
<h1>Welcome, <%= user.name %></h1>
<div class="bio"><%- user.bio %></div>

<!-- Output: -->
<!-- <h1>Welcome, John Doe</h1> -->
<!-- <div class="bio"><script>alert('XSS attack!');</script>This is my bio</div> -->

Performance Optimization

Cache Templates in Production

javascript
// In development
if (app.get('env') === 'development') {
app.disable('view cache');
} else {
// In production
app.enable('view cache');
}

Minimize Processing in Templates

Bad practice:

html
<% for(let i = 0; i < products.length; i++) { %>
<% const tax = products[i].price * 0.2; %>
<% const total = products[i].price + tax; %>
<div class="product">
<h3><%= products[i].name %></h3>
<p>Price: $<%= products[i].price.toFixed(2) %></p>
<p>Total with tax: $<%= total.toFixed(2) %></p>
</div>
<% } %>

Better practice:

javascript
// Process data before sending to template
app.get('/products', (req, res) => {
const products = getProducts();

// Process data before rendering
const enhancedProducts = products.map(product => {
const tax = product.price * 0.2;
return {
...product,
formattedPrice: product.price.toFixed(2),
tax: tax.toFixed(2),
total: (product.price + tax).toFixed(2)
};
});

res.render('pages/products', { products: enhancedProducts });
});
html
<!-- Cleaner template with minimal processing -->
<% products.forEach(product => { %>
<div class="product">
<h3><%= product.name %></h3>
<p>Price: $<%= product.formattedPrice %></p>
<p>Total with tax: $<%= product.total %></p>
</div>
<% }) %>

Avoid Synchronous Operations

Avoid file operations or database queries directly in templates. Prepare all data before rendering.

Client-Side Integration

Add Page-Specific CSS and JS

html
<!-- In your layout file (main.ejs) -->
<head>
<!-- Common CSS -->
<link rel="stylesheet" href="/css/main.css">

<!-- Page-specific CSS -->
<%- style %>
</head>
<body>
<!-- Content here -->

<!-- Common scripts -->
<script src="/js/main.js"></script>

<!-- Page-specific scripts -->
<%- script %>
</body>
html
<!-- In your page file (about.ejs) -->
<%- contentFor('style') %>
<link rel="stylesheet" href="/css/about.css">

<%- contentFor('script') %>
<script src="/js/about.js"></script>

<!-- Page content -->
<h1>About Us</h1>
<!-- more content -->

Passing Data to Client-Side JavaScript

html
<!-- Safely pass data to client-side JavaScript -->
<script>
// Use JSON.stringify to properly escape the data
const pageData = <%- JSON.stringify({
userId: user.id,
preferences: user.preferences,
permissions: user.permissions
}) %>;
</script>

Error Handling in Templates

Create Error Templates

javascript
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);

// Determine status code
const statusCode = err.statusCode || 500;

// Render appropriate error page
res.status(statusCode).render('errors/error', {
title: `Error ${statusCode}`,
message: app.get('env') === 'development' ? err.message : 'An error occurred',
stack: app.get('env') === 'development' ? err.stack : '',
statusCode: statusCode
});
});

// 404 handler
app.use((req, res) => {
res.status(404).render('errors/404', {
title: 'Page Not Found',
path: req.path
});
});
html
<!-- views/errors/error.ejs -->
<%- include('../partials/header', {title: title}) %>

<div class="error-container">
<h1><%= statusCode %> Error</h1>
<p><%= message %></p>

<% if (stack) { %>
<pre class="error-stack"><%= stack %></pre>
<% } %>

<p><a href="/">Return to home page</a></p>
</div>

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

Internationalization (i18n)

Consider implementing i18n from the beginning if your application might need multiple language support.

javascript
const i18n = require('i18n');

i18n.configure({
locales: ['en', 'es', 'fr'],
directory: path.join(__dirname, 'locales'),
defaultLocale: 'en',
cookie: 'locale'
});

app.use(i18n.init);

// Make i18n available to all templates
app.use((req, res, next) => {
res.locals.__ = res.__ = function() {
return i18n.__.apply(req, arguments);
};
next();
});
html
<!-- In your template -->
<h1><%= __('Welcome to our website') %></h1>
<p><%= __('Hello %s', user.name) %></p>

Real-World Example: A Blog Template

Let's implement a simple blog page with proper template structure and best practices:

Directory Structure

views/
├── layouts/
│ └── main.ejs
├── partials/
│ ├── header.ejs
│ ├── footer.ejs
│ ├── navigation.ejs
│ └── post-card.ejs
└── pages/
├── blog-list.ejs
└── blog-post.ejs

Route Handler

javascript
app.get('/blog', async (req, res) => {
try {
// Get blog posts from database
const posts = await BlogPost.find()
.sort({ createdAt: -1 })
.limit(10)
.populate('author');

// Format data before sending to template
const formattedPosts = posts.map(post => ({
id: post._id,
title: post.title,
excerpt: post.content.substring(0, 150) + '...',
formattedDate: new Date(post.createdAt).toLocaleDateString(),
author: post.author.name,
commentCount: post.comments.length
}));

res.render('pages/blog-list', {
title: 'Our Blog',
posts: formattedPosts,
activePage: 'blog'
});
} catch (err) {
next(err);
}
});

Layout Template

html
<!-- views/layouts/main.ejs -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><%= siteTitle %> | <%= title %></title>
<link rel="stylesheet" href="/css/main.css">
<%- style %>
</head>
<body>
<%- include('../partials/header') %>
<%- include('../partials/navigation') %>

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

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

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

Blog List Template

html
<!-- views/pages/blog-list.ejs -->
<%- contentFor('style') %>
<link rel="stylesheet" href="/css/blog.css">

<section class="blog-list">
<h1>Latest Articles</h1>

<div class="posts-container">
<% if (posts.length === 0) { %>
<p class="no-posts">No posts found.</p>
<% } else { %>
<% posts.forEach(post => { %>
<%- include('../partials/post-card', { post }) %>
<% }); %>
<% } %>
</div>

<div class="pagination">
<!-- Pagination controls -->
</div>
</section>

<%- contentFor('script') %>
<script src="/js/blog.js"></script>

Post Card Partial

html
<!-- views/partials/post-card.ejs -->
<article class="post-card">
<h2><a href="/blog/<%= post.id %>"><%= post.title %></a></h2>
<div class="post-meta">
<span class="date"><%= post.formattedDate %></span> by
<span class="author"><%= post.author %></span>
<span class="comments"><%= post.commentCount %> comments</span>
</div>
<p class="excerpt"><%= post.excerpt %></p>
<a href="/blog/<%= post.id %>" class="read-more">Read more</a>
</article>

Summary

Following these best practices for Express templates will help you create maintainable, secure, and efficient web applications:

  1. Organize templates using layouts, partials, and a clear directory structure
  2. Prioritize security by properly escaping output and validating input
  3. Optimize performance by processing data before rendering and enabling template caching in production
  4. Implement error handling with dedicated error templates
  5. Keep templates focused on presentation rather than complex logic
  6. Consider internationalization early if your project might need it
  7. Prepare data in route handlers, not in templates
  8. Use middleware to provide common data to all templates

By applying these best practices to your Express.js projects, you'll create more maintainable code and avoid common pitfalls that can lead to security issues or performance problems.

Additional Resources

Practice Exercises

  1. Convert an existing Express application to use partials and layouts
  2. Create a template with proper error handling and form validation
  3. Implement a multi-language template using i18n
  4. Build a blog template with pagination and category filtering
  5. Optimize a complex template by moving logic to the route handler


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)