handlebars vs mustache vs pug vs nunjucks vs ejs vs jade
Server-Side Template Engines for Node.js Applications
handlebarsmustachepugnunjucksejsjade

Server-Side Template Engines for Node.js Applications

ejs, handlebars, jade (deprecated), mustache, nunjucks, and pug are template engines used to generate HTML dynamically on the server side in Node.js environments. They allow developers to embed logic, iterate over data, and include partials within HTML structures, separating presentation from business logic. While jade has been renamed to pug, the legacy package remains in the ecosystem, often causing confusion. mustache and handlebars focus on logic-less templating, whereas ejs and nunjucks allow JavaScript-like expressions. pug offers a whitespace-sensitive syntax that reduces boilerplate. Choosing the right engine depends on your team's familiarity with HTML vs. indentation-based syntax, the need for logic within templates, and long-term maintenance considerations.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
handlebars41,542,24518,6792.96 MB1144 days agoMIT
mustache17,727,90316,718-1226 years agoMIT
pug3,790,80921,83222.5 kB3347 months agoMIT
nunjucks3,569,4069,0021.77 MB3633 years agoBSD-2-Clause
ejs08,124214 kB245 days agoApache-2.0
jade021,832-33411 years agoMIT

Server-Side Template Engines: ejs, handlebars, mustache, nunjucks, pug (and the legacy jade)

When building Node.js applications, rendering HTML on the server remains a common requirement for SEO, initial load performance, or simple email generation. The ecosystem offers several mature template engines, each with a distinct philosophy on syntax, logic, and extensibility. This comparison breaks down ejs, handlebars, mustache, nunjucks, and pug — while also addressing the deprecated jade package — to help you make an informed architectural choice.

⚠️ Critical Note: The Status of jade

Before diving into features, we must address jade. The jade templating engine was renamed to pug in 2016 due to trademark issues. The jade package on npm is deprecated and no longer receives security updates or bug fixes.

// ❌ DO NOT USE: Legacy jade package
const jade = require('jade'); 
// This package is unmaintained and may contain vulnerabilities
// ✅ USE THIS: Modern pug package
const pug = require('pug');
// Actively maintained, same syntax as jade but safer

If you are starting a new project, never install jade. If you are maintaining legacy code, plan a migration to pug. For the rest of this comparison, we will treat pug as the representative for the indentation-based syntax family.

📝 Syntax Style: HTML-like vs. Whitespace-Sensitive

The most immediate difference developers notice is the syntax. Some engines look like HTML with extra tags, while others use indentation to define structure.

ejs uses standard HTML with JavaScript embedded between <% %> tags. It feels familiar to anyone who has written PHP or ASP.

<!-- ejs: HTML-like syntax -->
<ul>
  <% users.forEach(function(user) { %>
    <li><%= user.name %></li>
  <% }); %>
</ul>

handlebars also uses HTML-like syntax but relies on double mustaches {{ }} for expressions and blocks.

<!-- handlebars: Block expressions -->
<ul>
  {{#each users}}
    <li>{{name}}</li>
  {{/each}}
</ul>

mustache is similar to Handlebars but strictly logic-less, using the same {{ }} syntax.

<!-- mustache: Logic-less sections -->
<ul>
  {{#users}}
    <li>{{name}}</li>
  {{/users}}
</ul>

nunjucks uses HTML-like syntax with {% %} for control flow and {{ }} for variables, inspired by Python's Jinja2.

<!-- nunjucks: Jinja2-style blocks -->
<ul>
  {% for user in users %}
    <li>{{ user.name }}</li>
  {% endfor %}
</ul>

pug (formerly jade) uses a whitespace-sensitive syntax that omits closing tags and angle brackets, resulting in less code.

// pug: Indentation-based syntax
ul
  each user in users
    li= user.name

🧠 Logic Capabilities: JavaScript vs. Logic-Less

A key architectural decision is how much logic you allow in your views. Some teams prefer strict separation, while others value the convenience of helper functions inside templates.

ejs allows full JavaScript execution inside templates. You can write loops, conditionals, and even define functions directly in the view.

<!-- ejs: Full JavaScript support -->
<% if (user.role === 'admin') { %>
  <p>Welcome, Administrator!</p>
<% } else { %>
  <p>Hello, User.</p>
<% } %>

handlebars is logic-less by design. You cannot write arbitrary JavaScript. You must use built-in helpers or register custom ones.

<!-- handlebars: Custom helpers required for logic -->
{{#if (eq user.role "admin")}}
  <p>Welcome, Administrator!</p>
{{else}}
  <p>Hello, User.</p>
{{/if}}

mustache is strictly logic-less. It supports sections and inverted sections but no custom helpers or arbitrary logic.

<!-- mustache: Inverted sections for else logic -->
{{#isAdmin}}
  <p>Welcome, Administrator!</p>
{{/isAdmin}}
{{^isAdmin}}
  <p>Hello, User.</p>
{{/isAdmin}}

nunjucks allows some logic but keeps it contained within its own template language. It supports filters and custom extensions.

<!-- nunjucks: Template-specific logic -->
{% if user.role == 'admin' %}
  <p>Welcome, Administrator!</p>
{% else %}
  <p>Hello, User.</p>
{% endif %}

pug allows JavaScript expressions but encourages keeping logic minimal. It supports mixins for reusable blocks.

// pug: JavaScript expressions in attributes
if user.role == 'admin'
  p Welcome, Administrator!
else
  p Hello, User.

🧩 Reusability: Partials, Includes, and Inheritance

Large applications require modular templates. Engines handle reuse differently, from simple includes to full template inheritance.

ejs supports includes via <%- include() %> but lacks native template inheritance. You often need to wrap layouts manually.

<!-- ejs: Simple includes -->
<%- include('partials/header') %>
<h1>Home Page</h1>
<%- include('partials/footer') %>

handlebars supports partials and block helpers, allowing for modular components.

<!-- handlebars: Partials -->
{{> header}}
<h1>Home Page</h1>
{{> footer}}

mustache supports partials using the > syntax.

<!-- mustache: Partials -->
{{> header}}
<h1>Home Page</h1>
{{> footer}}

nunjucks has powerful template inheritance using {% extends %} and {% block %}, similar to Django or Jinja2.

<!-- nunjucks: Template inheritance -->
{% extends "layout.html" %}
{% block content %}
  <h1>Home Page</h1>
{% endblock %}

pug supports both includes and extends, offering a clean syntax for inheritance.

// pug: Extends and blocks
extends layout

block content
  h1 Home Page

🚀 Async Rendering and Performance

Modern applications often need to fetch data during rendering. Not all engines handle asynchronous operations within templates equally.

ejs does not support async functions inside templates natively. You must resolve all data before passing it to the render function.

// ejs: Data must be resolved beforehand
const html = ejs.render(template, { users: await fetchUsers() });

handlebars supports async helpers in recent versions, allowing data fetching within the template context.

// handlebars: Async helpers possible
Handlebars.registerHelper('fetchUser', async (id) => {
  return await getUser(id);
});

mustache does not support async rendering. All data must be prepared in advance.

// mustache: Synchronous only
const output = Mustache.render(template, view);

nunjucks supports async rendering natively, allowing filters and functions to return promises.

// nunjucks: Async support enabled
nunjucks.render('page.html', { users: fetchUsers() }, (err, res) => {
  // Handles promises internally
});

pug supports async functions in mixins and includes when configured properly.

// pug: Async mixins
pug.renderFile('view.pug', { async: true }, (err, html) => {
  // Can handle async operations within template
});

🛡️ Security: Escaping and XSS Prevention

Security is critical when rendering user-generated content. All major engines provide escaping mechanisms, but defaults vary.

ejs escapes output by default when using <%= %> but allows unescaped output with <%- %>, which can be dangerous if misused.

<!-- ejs: Escaped vs Unescaped -->
<%= userInput %>   <!-- Escaped automatically -->
<%- userInput %>   <!-- Raw HTML (XSS risk if userInput is untrusted) -->

handlebars escapes expressions by default using {{ }}. Use triple mustaches {{{ }}} for raw HTML.

<!-- handlebars: Escaping -->
{{userInput}}   <!-- Escaped -->
{{{userInput}}} <!-- Raw HTML -->

mustache escapes by default. Use {{{ }}} for unescaped content.

<!-- mustache: Escaping -->
{{userInput}}   <!-- Escaped -->
{{{userInput}}} <!-- Raw HTML -->

nunjucks escapes by default. Use the safe filter to bypass escaping.

<!-- nunjucks: Escaping -->
{{ userInput }}      <!-- Escaped -->
{{ userInput | safe }} <!-- Raw HTML -->

pug escapes interpolated values by default. Use ! prefix for unescaped output.

// pug: Escaping
p= userInput   // Escaped
p!= userInput  // Raw HTML

📊 Summary: Key Differences

Featureejshandlebarsmustachenunjuckspug
SyntaxHTML + JS tagsHTML + {{ }}HTML + {{ }}HTML + {% %}Indentation
LogicFull JavaScriptHelpers onlyNoneTemplate logicExpressions
InheritanceManual (includes)PartialsPartialsExtends/BlocksExtends/Blocks
AsyncNoYes (helpers)NoYesYes
StatusMaintainedMaintainedMaintainedMaintainedMaintained

💡 The Big Picture

ejs is the pragmatic choice for teams who want zero learning curve. If your developers know JavaScript and HTML, they know EJS. It is perfect for simple views, email templates, or quick prototypes where strict separation of concerns is less critical.

handlebars is the balanced choice. It enforces cleaner views than EJS without being as restrictive as Mustache. Its ecosystem of helpers and partials makes it a robust standard for many Express applications.

mustache is the portable choice. Because it is logic-less and implemented in many languages, you can share templates between client and server or across different backend services. Use it when consistency across platforms matters more than convenience.

nunjucks is the power-user choice. If you are coming from Python/Django or need complex template inheritance and async features, Nunjucks offers the most capability. It is excellent for large content-heavy sites.

pug is the productivity choice. Its concise syntax reduces file size and typing. It is ideal for teams that prefer indentation-based languages (like Python or YAML) and want to write less boilerplate HTML.

Final Thought: Avoid jade entirely. For new projects, handlebars and pug are the most common modern choices. Pick handlebars if you want standard HTML structure, or pug if you want to write less code. Choose nunjucks if you need advanced template features, and ejs if you want simplicity above all.

How to Choose: handlebars vs mustache vs pug vs nunjucks vs ejs vs jade

  • handlebars:

    Choose handlebars if you need a logic-less template engine that enforces a clear separation between view and controller logic. It is suitable for projects that require robust partials, helpers, and a mature ecosystem with strong community support. It strikes a balance between simplicity and power, making it a safe default for many Express applications.

  • mustache:

    Choose mustache if you need a language-agnostic, logic-less templating system that can be shared across frontend and backend environments. It is best for scenarios where you want to guarantee no business logic leaks into views, as it strictly prohibits if/else blocks or custom helpers without pre-processing. It is lightweight and portable.

  • pug:

    Choose pug if you prefer a concise, whitespace-sensitive syntax that reduces HTML verbosity and speeds up writing views. It is well-suited for teams that value clean, indentation-based code and want built-in support for mixins, filters, and template inheritance. It is the modern successor to jade and is actively maintained.

  • nunjucks:

    Choose nunjucks if you want a powerful, feature-rich engine inspired by Jinja2 with support for inheritance, macros, and asynchronous rendering. It is ideal for large-scale applications where template organization, reuse, and complex logic within views are necessary. It offers the most flexibility among the logic-allowing engines.

  • ejs:

    Choose ejs if you want a simple, JavaScript-native templating syntax that feels like writing standard HTML with embedded JS. It is ideal for teams that prefer minimal abstraction and want to use existing JavaScript knowledge without learning a new DSL. It works well for small to medium projects where complex template inheritance is not required.

  • jade:

    Do NOT choose jade for new projects. The jade package was renamed to pug in 2016 and the original jade npm package is deprecated and no longer maintained. Using it introduces security risks and compatibility issues. If you encounter legacy code using jade, plan a migration to pug or another modern engine.

README for handlebars

CI Build Status jsDelivr Hits npm downloads npm version Bundle size Install size

Handlebars.js

Handlebars provides the power necessary to let you build semantic templates effectively with no frustration. Handlebars is largely compatible with Mustache templates. In most cases it is possible to swap out Mustache with Handlebars and continue using your current templates.

Checkout the official Handlebars docs site at handlebarsjs.com and try our live demo.

Installing

See our installation documentation.

Usage

In general, the syntax of Handlebars.js templates is a superset of Mustache templates. For basic syntax, check out the Mustache manpage.

Once you have a template, use the Handlebars.compile method to compile the template into a function. The generated function takes a context argument, which will be used to render the template.

var source = "<p>Hello, my name is {{name}}. I am from {{hometown}}. I have " +
             "{{kids.length}} kids:</p>" +
             "<ul>{{#kids}}<li>{{name}} is {{age}}</li>{{/kids}}</ul>";
var template = Handlebars.compile(source);

var data = { "name": "Alan", "hometown": "Somewhere, TX",
             "kids": [{"name": "Jimmy", "age": "12"}, {"name": "Sally", "age": "4"}]};
var result = template(data);

// Would render:
// <p>Hello, my name is Alan. I am from Somewhere, TX. I have 2 kids:</p>
// <ul>
//   <li>Jimmy is 12</li>
//   <li>Sally is 4</li>
// </ul>

Full documentation and more examples are at handlebarsjs.com.

Precompiling Templates

Handlebars allows templates to be precompiled and included as javascript code rather than the handlebars template allowing for faster startup time. Full details are located here.

Differences Between Handlebars.js and Mustache

Handlebars.js adds a couple of additional features to make writing templates easier and also changes a tiny detail of how partials work.

Block expressions have the same syntax as mustache sections but should not be confused with one another. Sections are akin to an implicit each or with statement depending on the input data and helpers are explicit pieces of code that are free to implement whatever behavior they like. The mustache spec defines the exact behavior of sections. In the case of name conflicts, helpers are given priority.

Compatibility

There are a few Mustache behaviors that Handlebars does not implement.

  • Handlebars deviates from Mustache slightly in that it does not perform recursive lookup by default. The compile time compat flag must be set to enable this functionality. Users should note that there is a performance cost for enabling this flag. The exact cost varies by template, but it's recommended that performance sensitive operations should avoid this mode and instead opt for explicit path references.
  • The optional Mustache-style lambdas are not supported. Instead Handlebars provides its own lambda resolution that follows the behaviors of helpers.
  • Alternative delimiters are not supported.

Supported Environments

Handlebars has been designed to work in any ECMAScript 3 environment. This includes

  • Node.js
  • Chrome
  • Firefox
  • Safari 5+
  • Opera 11+
  • IE 6+

Older versions and other runtimes are likely to work but have not been formally tested. The compiler requires JSON.stringify to be implemented natively or via a polyfill. If using the precompiler this is not necessary.

Performance

In a rough performance test, precompiled Handlebars.js templates (in the original version of Handlebars.js) rendered in about half the time of Mustache templates. It would be a shame if it were any other way, since they were precompiled, but the difference in architecture does have some big performance advantages. Justin Marney, a.k.a. gotascii, confirmed that with an independent test. The rewritten Handlebars (current version) is faster than the old version, with many performance tests being 5 to 7 times faster than the Mustache equivalent.

Upgrading

See release-notes.md for upgrade notes.

Known Issues

See FAQ.md for known issues and common pitfalls.

Handlebars in the Wild

  • Assemble, by @jonschlinkert and @doowb, is a static site generator that uses Handlebars.js as its template engine.
  • Cory, by @leo, is another tiny static site generator
  • CoSchedule An editorial calendar for WordPress that uses Handlebars.js
  • dashbars A modern helper library for Handlebars.js.
  • Ember.js makes Handlebars.js the primary way to structure your views, also with automatic data binding support.
  • Ghost Just a blogging platform.
  • handlebars_assets: A Rails Asset Pipeline gem from Les Hill (@leshill).
  • handlebars-helpers is an extensive library with 100+ handlebars helpers.
  • handlebars-layouts is a set of helpers which implement extendible and embeddable layout blocks as seen in other popular templating languages.
  • hbs: An Express.js view engine adapter for Handlebars.js, from Don Park.
  • koa-hbs: koa generator based renderer for Handlebars.js.
  • jblotus created http://tryhandlebarsjs.com for anyone who would like to try out Handlebars.js in their browser.
  • jQuery plugin: allows you to use Handlebars.js with jQuery.
  • Lumbar provides easy module-based template management for handlebars projects.
  • Marionette.Handlebars adds support for Handlebars and Mustache templates to Marionette.
  • sammy.js by Aaron Quint, a.k.a. quirkey, supports Handlebars.js as one of its template plugins.
  • SproutCore uses Handlebars.js as its main templating engine, extending it with automatic data binding support.
  • YUI implements a port of handlebars
  • Swag by @elving is a growing collection of helpers for handlebars.js. Give your handlebars.js templates some swag son!
  • DOMBars is a DOM-based templating engine built on the Handlebars parser and runtime DEPRECATED
  • promised-handlebars is a wrapper for Handlebars that allows helpers to return Promises.
  • just-handlebars-helpers A fully tested lightweight package with common Handlebars helpers.

External Resources

Have a project using Handlebars? Send us a pull request!

License

Handlebars.js is released under the MIT license.