ejs, handlebars, nunjucks, and pug are popular templating engines used to generate dynamic HTML on the server side within Node.js applications. They allow developers to separate presentation logic from business logic by embedding variables, control structures, and reusable components into static templates. While they all serve the same core purpose, they differ significantly in syntax style, logic capabilities, and how they handle template inheritance. Choosing the right one depends on your team's familiarity with JavaScript, preference for HTML-like structures, and requirements for template reuse.
When building server-rendered Node.js applications, selecting a templating engine is a foundational decision that affects developer velocity, maintainability, and security. ejs, handlebars, nunjucks, and pug are the four most established options in the ecosystem. While they all compile templates into HTML, their underlying philosophies differ — from embedding raw JavaScript to enforcing logic-less views. Let's examine how they handle real-world engineering challenges.
The most immediate difference is how closely the template syntax resembles standard HTML. This impacts how easily frontend developers can edit templates without deep backend knowledge.
ejs keeps standard HTML intact.
<% and %> tags.<!-- ejs: Standard HTML structure -->
<div class="user-card">
<h1><%= user.name %></h1>
<% if (user.isActive) { %>
<p>Active Member</p>
<% } %>
</div>
handlebars uses double mustaches for expressions.
<!-- handlebars: Mustache syntax -->
<div class="user-card">
<h1>{{user.name}}</h1>
{{#if user.isActive}}
<p>Active Member</p>
{{/if}}
</div>
nunjucks uses Jinja2-style delimiters.
{% %} and variables use {{ }}.<!-- nunjucks: Jinja2 style -->
<div class="user-card">
<h1>{{ user.name }}</h1>
{% if user.isActive %}
<p>Active Member</p>
{% endif %}
</div>
pug removes HTML tags' closing brackets and uses indentation.
<!-- pug: Indentation based -->
.user-card
h1= user.name
if user.isActive
p Active Member
How much JavaScript can you run inside the template? This determines where business logic lives.
ejs allows raw JavaScript execution.
// ejs: Raw JS loops
<ul>
<% items.forEach(function(item) { %>
<li><%= item.title %></li>
<% }); %>
</ul>
handlebars restricts logic to helpers.
// handlebars: Helper based loops
<ul>
{{#each items}}
<li>{{title}}</li>
{{/each}}
</ul>
nunjucks offers built-in control structures.
// nunjucks: Built-in control flow
<ul>
{% for item in items %}
<li>{{ item.title }}</li>
{% endfor %}
</ul>
pug uses JavaScript-like syntax for logic.
// pug: Indentation logic
ul
each item in items
li= item.title
Large applications need reusable layouts. How do these engines handle composing pages from smaller pieces?
ejs relies on includes.
<!-- ejs: Manual includes -->
<%- include('partials/header') %>
<h1>Page Content</h1>
<%- include('partials/footer') %>
handlebars uses partials registration.
<!-- handlebars: Partials -->
{{> header}}
<h1>Page Content</h1>
{{> footer}}
nunjucks supports full template inheritance.
<!-- nunjucks: Inheritance -->
<!-- base.html -->
<html><body>{% block content %}{% endblock %}</body></html>
<!-- child.html -->
{% extends "base.html" %}
{% block content %}
<h1>Page Content</h1>
{% endblock %}
pug supports includes and extends.
// pug: Extends and Blocks
// layout.pug
html
body
block content
// index.pug
extends layout
block content
h1 Page Content
Preventing Cross-Site Scripting (XSS) is critical. How does each engine handle untrusted user input?
ejs escapes by default with <%=.
<%- for unescaped output (dangerous).// ejs: Escaping
// Safe: escapes HTML entities
<div><%= userInput %></div>
// Unsafe: renders raw HTML
<div><%- userInput %></div>
handlebars escapes by default with {{.
{{{ for unescaped output.// handlebars: Escaping
// Safe
<div>{{userInput}}</div>
// Unsafe
<div>{{{userInput}}}</div>
nunjucks escapes by default with {{.
safe filter to allow HTML.// nunjucks: Escaping
// Safe
<div>{{ userInput }}</div>
// Unsafe (explicitly marked)
<div>{{ userInput | safe }}</div>
pug escapes by default with =.
!= for unescaped output.// pug: Escaping
// Safe
div= userInput
// Unsafe
div!= userInput
| Feature | ejs | handlebars | nunjucks | pug |
|---|---|---|---|---|
| Syntax | HTML + JS tags | HTML + Mustache | HTML + Jinja2 | Indentation |
| Logic | Full JavaScript | Logic-less (Helpers) | Built-in Control | JS-like |
| Inheritance | Includes only | Partials only | Block Inheritance | Extends + Mixins |
| Escaping | <%= (Safe) | {{ (Safe) | {{ (Safe) | = (Safe) |
| Learning Curve | Low | Medium | Medium | High |
ejs is the pragmatic choice for teams who want to get started quickly without learning a new syntax. It feels like writing HTML with script tags. However, the freedom to write raw JS can lead to messy templates if not governed by strict code reviews.
handlebars is the disciplined choice. By forcing logic into helpers, it keeps templates dumb and fast. This is perfect for design systems where templates might be edited by non-developers or shared across different platforms.
nunjucks is the architectural choice for large sites. Its inheritance model is superior for maintaining complex layouts with multiple content regions. If you come from a Django or Jinja background, this will feel like home.
pug is the productivity choice for backend-heavy teams. It reduces boilerplate significantly. However, the whitespace sensitivity can cause frustrating bugs if indentation is inconsistent, and it creates a barrier for frontend developers used to standard HTML.
Final Thought: All four packages are mature and stable. None are deprecated. Your decision should rest on team preference and template complexity. For simple views, ejs or pug work well. For complex layouts, nunjucks shines. For strict separation, handlebars is best.
Choose handlebars if you need a logic-less template system that enforces a strict separation between view and controller. It is excellent for projects where templates are shared between server and client, as the syntax is consistent across environments. Use it when you want to prevent complex logic from creeping into your HTML.
Choose ejs if your team wants minimal learning overhead and prefers writing standard HTML with embedded JavaScript logic. It is ideal for projects where developers want full access to JavaScript features inside templates without learning a new syntax. It works well for simple views or when migrating legacy ASP/JSP styles to Node.js.
Choose nunjucks if you need powerful template inheritance and block overriding similar to Python's Jinja2 or Django. It is suitable for large applications with deeply nested layouts where content sections need to be injected into a master frame. It offers a good balance between logic capabilities and safety.
Choose pug if you prefer a concise, whitespace-sensitive syntax that reduces typing and eliminates closing tags. It is best for teams that value brevity and are comfortable with indentation-based structures. Avoid it if your designers or frontend developers need to edit raw HTML files directly, as the syntax diverges significantly from standard HTML.
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.
See our installation documentation.
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.
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.
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.
There are a few Mustache behaviors that Handlebars does not implement.
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.Handlebars has been designed to work in any ECMAScript 3 environment. This includes
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.
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.
See release-notes.md for upgrade notes.
See FAQ.md for known issues and common pitfalls.
Have a project using Handlebars? Send us a pull request!
Handlebars.js is released under the MIT license.