Skip to content

Handlebars Templating with Express

Handlebars is a logic-minimal templating engine using mustache-style {{ }} placeholders, intentionally restricting what you can do inside a template to keep view logic out of the presentation layer. This lesson covers setting it up with express-handlebars.

Handlebars Syntax Basics

Handlebars uses double curly braces, {{ }}, for escaped output, and triple braces, {{{ }}}, for raw, unescaped HTML. Unlike EJS or Pug, Handlebars deliberately does not allow arbitrary JavaScript expressions inside templates, only variable lookups and a small set of built-in and custom "helpers".

Express itself doesn't ship a Handlebars integration, express-handlebars is the standard package that wires Handlebars into Express's res.render() and view engine system.

const express = require('express');
const { engine } = require('express-handlebars');

const app = express();
app.engine('handlebars', engine());
app.set('view engine', 'handlebars');
app.set('views', './views');

app.get('/profile', (req, res) => {
  res.render('profile', { name: 'Ada' });
});

app.engine('handlebars', engine()) registers the Handlebars rendering function for the .handlebars file extension.

Handlebars Syntax Reference

{{ value }}        escaped output
{{{ html }}}       raw, unescaped output
{{#if condition}} ... {{/if}}
{{#each items}} ... {{/each}}
{{> partialName}}   render a partial
  • Handlebars is intentionally "logic-less", complex logic belongs in helpers or your route handler, not the template.
  • Built-in block helpers cover the essentials: if, unless, each, with.
  • Custom helpers extend Handlebars with your own reusable template functions.
  • {{> partialName}} renders a registered partial, similar to includes in other engines.

Handlebars Cheatsheet

Common Handlebars syntax for output, control flow, and partials.

Task Syntax
Escaped output {{ user.name }}
Raw HTML output {{{ trustedHtml }}}
Conditional {{#if loggedIn}} ... {{else}} ... {{/if}}
Loop {{#each products}} {{this.name}} {{/each}}
Partial {{> header}}
Layout layoutsDir + defaultLayout option in engine()

Writing Custom Helpers

When a template needs logic beyond the built-in helpers, formatting a date, pluralizing a word, define a custom helper function and register it when configuring the engine.

app.engine('handlebars', engine({
  helpers: {
    formatPrice: (cents) => `$${(cents / 100).toFixed(2)}`,
  },
}));

Using the helper in a template

<p>Price: {{ formatPrice product.priceInCents }}</p>

Layouts With express-handlebars

express-handlebars includes built-in layout support, a shared wrapper template rendered around every page's content, configured once instead of repeated per template.

app.engine('handlebars', engine({
  layoutsDir: './views/layouts',
  defaultLayout: 'main',
}));

//- views/layouts/main.handlebars
<!DOCTYPE html>
<html>
  <body>
    {{{body}}}
  </body>
</html>

Common Mistakes

  • Trying to write arbitrary JavaScript expressions directly in a template, Handlebars deliberately doesn't support this.
  • Using {{{ }}} for user-supplied content, bypassing escaping and risking XSS.
  • Forgetting to register app.engine('handlebars', engine()) before setting the view engine.
  • Duplicating the same page wrapper markup in every template instead of using layouts.

Key Takeaways

  • Handlebars uses mustache-style {{ }} placeholders and is intentionally logic-minimal.
  • express-handlebars wires Handlebars into Express's view engine system.
  • Built-in helpers (if, each, with) and custom helpers cover most templating needs.
  • Built-in layout support avoids duplicating shared page structure across templates.

Pro Tip

When you find yourself wanting to write real JavaScript logic inside a Handlebars template, that's a signal to move the logic into a custom helper or, better, prepare the final data shape in your route handler before rendering.