NodeJSHandlebars

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

Bash
npm install express-handlebars

app.js

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
})
`app.engine()` registers an engine Express doesn't know by name
For built-in-supported engines you only call `app.set('view engine', ...)`. Handlebars needs an explicit `app.engine('handlebars', engine(...))` first, because you're wiring in the `express-handlebars` integration (with its layout support). After that, `res.render` works the same. `defaultLayout: 'main'` means every view is wrapped in `views/layouts/main.handlebars` unless overridden.
Expressions — escaped by default

Text
{{ 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 }}
Double `{{ }}` escapes; triple `{{{ }}}` does NOT
Handlebars escapes `{{ value }}` by default and emits raw HTML for `{{{ value }}}`. The triple-stache is the same loaded weapon as [EJS `<%-`](/nodejs/ejs) and [Pug `!=`](/nodejs/pug): use it only for trusted, server-generated markup, never for user-supplied content, or you've created an [XSS](/nodejs/cors) hole. The extra brace is easy to add by reflex — don't.
Block helpers — the limited control flow

views/home.handlebars

Text
<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}}
No arbitrary expressions — `{{#if x > 5}}` is NOT valid
This is the defining limitation: Handlebars built-ins (`if`, `unless`, `each`, `with`) only test **truthiness** or iterate — you cannot write comparisons, arithmetic, or method calls inline. `{{#if age > 18}}` is a syntax error. To compare, you either compute the boolean in your route (`{ isAdult: age > 18 }`) or register a **helper**. This friction is intentional: it pushes logic out of the template.
Custom helpers — where logic actually lives

JS
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

Text
<p>Total: {{money order.total}}</p>
<p>Joined: {{formatDate user.createdAt}}</p>

{{#if (gt user.age 18)}}
  <p>Adult content available</p>
{{/if}}
Helpers are plain JS functions — testable and reusable
A helper is just a function you register; templates call it by name. This is the Handlebars philosophy in action: formatting, comparisons, and derived values are **JavaScript you can unit-test**, not logic buried in markup. Register `gt`/`eq`/`formatDate`-style helpers once and reuse them everywhere. The result is templates that read as pure presentation.
Layouts and partials

views/layouts/main.handlebars

Text
<!DOCTYPE html>
<html>
<head><title>{{title}}</title></head>
<body>
  {{> nav}}                <!-- {{> name }} renders a partial -->
  {{{body}}}               <!-- the rendered page is injected here -->
  {{> footer}}
</body>
</html>
`{{{body}}}` is where the page goes; `{{> name}}` includes a partial
With `express-handlebars`, the layout wraps every view and exposes the page's rendered HTML as `{{{body}}}` (raw, because it's trusted server-generated markup). Reusable fragments registered as **partials** are included with `{{> partialName}}`, optionally passing context: `{{> card title="Hi"}}`. Put partials in `views/partials` and they're auto-discovered.
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)

The constraint is the feature
Handlebars' logic-less design frustrates people who just want `{{#if a > b}}` — but that frustration is the guardrail working. By making logic *inconvenient* in templates, it keeps presentation and logic separate, which pays off as projects grow. If that discipline appeals, Handlebars is excellent; if you'd rather have the flexibility (and responsibility) of inline code, [EJS](/nodejs/ejs) is the friendlier choice.
Next
Move from rendering to guarding inputs — the validation section: [Why Validate Input?](/nodejs/validation-intro).