Express template engines let you generate dynamic HTML on the server before sending it to the client. EJS and Handlebars are two popular choices, each with its own syntax and philosophy.
Here’s a real-time EJS example in action. Imagine a simple Express app:
const express = require('express');
const app = express();
const path = require('path');
// Set EJS as the view engine
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.get('/', (req, res) => {
const user = { name: 'Alice', isAdmin: true };
const posts = [
{ title: 'First Post', author: 'Alice' },
{ title: 'Second Post', author: 'Bob' }
];
res.render('index', { user, posts });
});
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
And a corresponding views/index.ejs file:
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Hello, <%= user.name %>!</h1>
<% if (user.isAdmin) { %>
<p>Welcome, Administrator!</p>
<% } %>
<h2>Latest Posts</h2>
<ul>
<% posts.forEach(post => { %>
<li><%= post.title %> by <%= post.author %></li>
<% }); %>
</ul>
</body>
</html>
When a request hits the root route, Express finds views/index.ejs, injects the user and posts data into it, and sends the resulting HTML to the browser. The <%= user.name %> part is EJS syntax for outputting the name property of the user object. The <% if (user.isAdmin) { %> ... <% } %> is EJS for control flow, like an if statement.
Handlebars offers a similar capability but with a slightly different syntax and a stronger emphasis on logic-less templates. Here’s how the Handlebars equivalent might look, assuming you’ve configured Handlebars as your engine (e.g., using express-handlebars):
// ... (Express setup similar to above, but with Handlebars configuration)
// app.engine('hbs', require('express-handlebars')());
// app.set('view engine', 'hbs');
// app.set('views', path.join(__dirname, 'views'));
app.get('/', (req, res) => {
const data = {
user: { name: 'Bob', isAdmin: false },
posts: [
{ title: 'My First Handlebars Post', author: 'Bob' },
{ title: 'Another One', author: 'Charlie' }
]
};
res.render('index', data);
});
And a views/index.hbs file:
<!DOCTYPE html>
<html>
<head>
<title>Welcome</title>
</head>
<body>
<h1>Hello, {{user.name}}!</h1>
{{#if user.isAdmin}}
<p>Welcome, Administrator!</p>
{{/if}}
<h2>Latest Posts</h2>
<ul>
{{#each posts}}
<li>{{this.title}} by {{this.author}}</li>
{{/each}}
</ul>
</body>
</html>
In Handlebars, {{user.name}} is used for outputting data. Control flow uses double curly braces with special keywords like {{#if}} and {{#each}}. The {{this}} refers to the current item in an iteration.
The core problem these engines solve is separating presentation logic from application logic. Instead of embedding HTML strings directly within your JavaScript, you create separate template files. This makes your code cleaner, more maintainable, and easier for designers to work with. Both EJS and Handlebars allow you to define custom "helpers" – functions that encapsulate reusable logic or complex rendering tasks within your templates. For example, you might create a helper to format dates or to render a specific UI component.
The most surprising thing about using template engines like EJS and Handlebars effectively is how much less logic you actually need in the template itself. While they offer control flow (if/else, loops), the best practice is to prepare your data before rendering. This means performing data transformations, filtering, and complex calculations in your Express route handlers or dedicated service layers, and then passing a clean, presentation-ready data object to the template. This keeps templates focused solely on displaying data, making them easier to read and less prone to bugs.
The next step is often exploring template inheritance or partials, which allow you to reuse common layout elements like headers and footers across multiple pages without duplication.