Handlebars
Handlebars is a logic-less template engine: it uses {{ }} "mustache" syntax and deliberately restricts how much logic you can put in a template. You can't write arbitrary JavaScript inline the way you can with EJS — instead you expose data and named helpers from your code. This constraint is the whole point: it forces presentation logic out of templates and into testable code. This page covers expressions, blocks, helpers, and partials.
Setup with express-handlebars
npm install express-handlebars
app.js
import { engine } from 'express-handlebars'
app.engine('handlebars', engine({ defaultLayout: 'main' }))
app.set('view engine', 'handlebars')
app.set('views', './views')
app.get('/', (req, res) => {
res.render('home', { title: 'Home', users }) // views/home.handlebars
})Expressions — escaped by default
{{ user.name }} <!-- output, HTML-escaped (safe) -->
{{{ user.bio }}} <!-- TRIPLE braces = raw, unescaped (dangerous) -->
{{ user.address.city }} <!-- dotted path into nested data -->
{{! this is a comment }}Block helpers — the limited control flow
views/home.handlebars
<h1>{{title}}</h1>
{{#if user.isAdmin}}
<a href="/admin">Admin panel</a>
{{else}}
<p>Standard account</p>
{{/if}}
<ul>
{{#each users}}
<li>{{this.name}} — {{this.email}}</li> <!-- 'this' = current item -->
{{else}}
<li>No users found.</li> <!-- runs when the list is empty -->
{{/each}}
</ul>
{{#unless user.verified}}
<p>Please verify your email.</p>
{{/unless}}Custom helpers — where logic actually lives
app.engine('handlebars', engine({
defaultLayout: 'main',
helpers: {
// Formatting helper: {{money order.total}}
money: (cents) => '$' + (cents / 100).toFixed(2),
// Comparison helper: {{#if (gt age 18)}} ... {{/if}}
gt: (a, b) => a > b,
// Date helper: {{formatDate createdAt}}
formatDate: (d) => new Date(d).toLocaleDateString(),
},
}))Using the helpers in a template
<p>Total: {{money order.total}}</p>
<p>Joined: {{formatDate user.createdAt}}</p>
{{#if (gt user.age 18)}}
<p>Adult content available</p>
{{/if}}Layouts and partials
views/layouts/main.handlebars
<!DOCTYPE html>
<html>
<head><title>{{title}}</title></head>
<body>
{{> nav}} <!-- {{> name }} renders a partial -->
{{{body}}} <!-- the rendered page is injected here -->
{{> footer}}
</body>
</html>When Handlebars fits
Choose Handlebars when… | Prefer EJS/Pug when… |
|---|---|
You want logic OUT of templates by design | You want full JS in templates (EJS) |
Templates are edited by designers/non-devs | You prefer terse indentation (Pug) |
You value clean separation & testable helpers | You want zero ceremony for one-off logic |
You share templates with a frontend (Mustache-compatible) | Inheritance/mixins matter most (Pug) |