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 feedback@compilenrun.com. I’d greatly appreciate your feedback! :)