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:
Engine | Syntax | Learning Curve | Features |
---|---|---|---|
EJS | HTML with <% %> tags | Low | Simple, similar to PHP/ASP |
Pug | Indentation-based, minimal | Medium | Concise, powerful features |
Handlebars | HTML with {{}} expressions | Low | Logic-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.
Recommended Structure
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:
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:
// In your Express route
app.get('/', (req, res) => {
res.render('pages/home', {
title: 'Home Page',
user: { name: 'John' }
});
});
<!-- 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') %>
<!-- 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:
const expressLayouts = require('express-ejs-layouts');
// Use layouts
app.use(expressLayouts);
app.set('layout', 'layouts/main');
<!-- 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
// 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
// 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}
orp= content
- Escaped output!{content}
orp!= 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
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 });
});
<!-- Safe (EJS) -->
<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> -->
<!-- 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
// In development
if (app.get('env') === 'development') {
app.disable('view cache');
} else {
// In production
app.enable('view cache');
}
Minimize Processing in Templates
Bad practice:
<% 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:
// 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 });
});
<!-- 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
<!-- 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>
<!-- 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
<!-- 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
// 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
});
});
<!-- 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.
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();
});
<!-- 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
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
<!-- 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
<!-- 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
<!-- 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:
- Organize templates using layouts, partials, and a clear directory structure
- Prioritize security by properly escaping output and validating input
- Optimize performance by processing data before rendering and enabling template caching in production
- Implement error handling with dedicated error templates
- Keep templates focused on presentation rather than complex logic
- Consider internationalization early if your project might need it
- Prepare data in route handlers, not in templates
- 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
- Express.js Documentation on Template Engines
- EJS Documentation
- Pug Documentation
- Handlebars Documentation
- express-ejs-layouts
- i18n for Node.js
Practice Exercises
- Convert an existing Express application to use partials and layouts
- Create a template with proper error handling and form validation
- Implement a multi-language template using i18n
- Build a blog template with pagination and category filtering
- 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! :)