ejs, handlebars, mustache, pug, and squirrelly are JavaScript templating engines used to generate HTML (or other text formats) on the server side. They differ primarily in their syntax philosophy, security models, and feature sets. ejs uses standard JavaScript logic within tags, offering maximum flexibility but requiring manual security hygiene. handlebars and mustache rely on a logic-less approach with custom helpers, prioritizing separation of concerns. pug (formerly Jade) uses a significant whitespace syntax to reduce boilerplate, while squirrelly offers a logic-optional, fast, and secure alternative that works in both Node and the browser without compilation steps.
When building server-rendered applications in Node.js, choosing a templating engine is a foundational architectural decision. The packages ejs, handlebars, mustache, pug, and squirrelly all solve the same problemβmerging data with HTMLβbut they take vastly different approaches to syntax, logic execution, and security. Let's break down how they handle real-world engineering challenges.
The most immediate difference developers encounter is how the template language feels to write. Some engines let you write raw JavaScript, while others enforce a custom Domain Specific Language (DSL).
ejs embeds standard JavaScript directly inside tags using <% %>.
.js file.<!-- ejs: Standard JS logic -->
<ul>
<% users.forEach(function(user) { %>
<li><%= user.name %></li>
<% }); %>
</ul>
handlebars uses a mustache-based syntax {{ }} with custom helpers for logic.
<!-- handlebars: Helper-based logic -->
<ul>
{{#each users}}
<li>{{name}}</li>
{{/each}}
</ul>
mustache adheres strictly to the logic-less specification.
{{#section}}) for loops and conditionals but allows no custom helpers or complex logic.<!-- mustache: Strictly logic-less -->
<ul>
{{#users}}
<li>{{name}}</li>
{{/users}}
</ul>
pug uses significant whitespace and indentation instead of closing tags.
> or </div>.// pug: Whitespace-sensitive syntax
ul
each user in users
li= user.name
squirrelly uses a syntax similar to Mustache but allows optional JavaScript logic.
{{ }} for variables and {{if}} for logic blocks.<!-- squirrelly: Logic-optional -->
<ul>
{{for user in users}}
<li>{{user.name}}</li>
{{/for}}
</ul>
Cross-Site Scripting (XSS) is the primary risk in templating. Engines handle output escaping differently, which directly impacts your security posture.
ejs does not escape output by default when using <%= %>.
<%- %> for raw HTML and remember that <%= %> escapes special characters.// ejs: Manual attention required
// <%= userInput %> -> Escapes HTML (Safe)
// <%- userInput %> -> Raw HTML (Unsafe if userInput is from user)
const html = ejs.render('<div><%= userInput %></div>', { userInput: '<script>alert(1)</script>' });
// Output: <div><script>alert(1)</script></div>
handlebars escapes all expressions {{ }} by default.
{{{ }}} or the allowProtoMethodsByDefault option carefully.// handlebars: Auto-escapes by default
const template = Handlebars.compile('<div>{{input}}</div>');
const html = template({ input: '<script>alert(1)</script>' });
// Output: <div><script>alert(1)</script></div>
mustache also escapes by default.
// mustache: Auto-escapes by default
const html = Mustache.render('<div>{{input}}</div>', { input: '<script>alert(1)</script>' });
// Output: <div><script>alert(1)</script></div>
pug escapes interpolated values by default.
= escapes content, while != renders raw HTML.// pug: Auto-escapes interpolations
const fn = pug.compile('div= userInput');
const html = fn({ userInput: '<script>alert(1)</script>' });
// Output: <div><script>alert(1)</script></div>
squirrelly auto-escapes all variables by default.
{{raw}} if you intentionally need unescaped HTML.// squirrelly: Auto-escapes by default
const html = Sqrl.render('<div>{{input}}</div>', { input: '<script>alert(1)</script>' });
// Output: <div><script>alert(1)</script></div>
When templates need to format dates, currency, or perform complex checks, the engine must provide a way to extend functionality.
ejs relies on native JavaScript functions passed in the scope.
// ejs: Pass functions directly
ejs.render('<%= formatDate(date) %>', {
date: new Date(),
formatDate: (d) => d.toISOString()
});
handlebars requires registering named helpers.
// handlebars: Register helpers
Handlebars.registerHelper('formatDate', (date) => date.toISOString());
const template = Handlebars.compile('{{formatDate date}}');
mustache allows passing functions in the data object but lacks a formal helper registry.
// mustache: Functions in data view
Mustache.render('{{formattedDate}}', {
date: new Date(),
formattedDate: function() { return this.date.toISOString(); }
});
pug supports mixins and includes for reusable components.
// pug: Mixins for reusability
mixin dateFormat(date)
li= date.toISOString()
ul
+dateFormat(new Date())
squirrelly supports filters and plugins.
// squirrelly: Custom filters
Sqrl.defineFilter('upper', (str) => str.toUpperCase());
Sqrl.render('{{name | upper}}', { name: 'alice' });
How the template is processed affects server latency and deployment strategies.
ejs compiles templates into JavaScript functions at runtime (or pre-compile time).
new Function() constructor internally.// ejs: Compiles to JS function
const fn = ejs.compile('<h1><%= title %></h1>');
// fn is a reusable JavaScript function
handlebars compiles templates into spec-compliant JavaScript.
// handlebars: Compile step
const template = Handlebars.compile('<h1>{{title}}</h1>');
mustache interprets the template string on every render unless pre-compiled.
// mustache: Interpretation
Mustache.render('<h1>{{title}}</h1>', { title: 'Hello' });
pug is strictly a compile-time engine.
pug.render in production without a compilation step.// pug: Must be compiled
const fn = pug.compile('h1= title');
// Renders extremely fast as optimized JS
squirrelly compiles templates to JavaScript functions similar to EJS.
// squirrelly: Compiles to JS
const tmpl = Sqrl.compile('<h1>{{title}}</h1>');
Some teams need to render templates on the client side (e.g., for email previews or static site generation in the browser).
ejs can run in the browser but requires bundling the whole engine.
// ejs: Browser usage requires bundling
// <script src="ejs.js"></script>
// ejs.render(template, data);
handlebars has a dedicated runtime for the browser.
// handlebars: Browser runtime
// Load handlebars.runtime.js
// Execute pre-compiled template function
mustache is fully language-agnostic and has tiny browser implementations.
// mustache: Tiny browser footprint
Mustache.render(template, view);
pug is primarily a server-side build tool.
// pug: Typically build-time only
// Compiled to HTML via Webpack/Gulp plugin
squirrelly is designed for isomorphic usage out of the box.
// squirrelly: Isomorphic
// Works identically in Node and Browser
Sqrl.render(template, data);
Despite their differences, these libraries share core objectives and capabilities.
// Common usage across all: Variable insertion
// EJS: <%= name %>
// Handlebars: {{name}}
// Mustache: {{name}}
// Pug: = name
// Squirrelly: {{name}}
// Conceptual equivalence
// EJS: <% if (active) { %> ... <% } %>
// Handlebars: {{#if active}} ... {{/if}}
// Mustache: {{#active}} ... {{/active}}
// Pug: if active
// Squirrelly: {{if active}} ... {{/if}}
// Common pattern: Including a partial
// EJS: <%- include('header') %>
// Handlebars: {{> header}}
// Mustache: {{> header}}
// Pug: include header.pug
// Squirrelly: {{include file="header"}}
| Feature | ejs | handlebars | mustache | pug | squirrelly |
|---|---|---|---|---|---|
| Syntax Style | Embedded JS | Logic-less + Helpers | Strict Logic-less | Whitespace/Indentation | Logic-optional + Filters |
| Security | Manual (Escape needed) | Auto-escaped | Auto-escaped | Auto-escaped | Auto-escaped |
| Logic Power | High (Full JS) | Medium (Helpers) | Low (None) | High (JS + Mixins) | Medium (Expressions) |
| Compilation | Runtime or Pre | Runtime or Pre | Interpret or Pre | Compile-time Only | Runtime or Pre |
| Browser Ready | Heavy | Yes (Runtime) | Yes (Tiny) | Rare (Build step) | Yes (Isomorphic) |
| Learning Curve | Low (If you know JS) | Medium | Low | High (New syntax) | Low |
ejs is the pragmatic choice for teams who want to leverage existing JavaScript knowledge without learning a new syntax. It is powerful but demands discipline regarding security. Use it for internal tools or when rapid prototyping with full JS access is needed.
handlebars remains the industry standard for balanced templating. Its helper system provides a clean extension point without compromising the logic-less philosophy. It is the safest bet for large-scale applications where maintainability and separation of concerns are critical.
mustache is the specialist for portability. If your templates need to run in Ruby, Python, and JavaScript without modification, or if you need a tiny browser library, this is the only correct choice. Avoid it for complex app logic.
pug is the productivity booster for developers who hate HTML boilerplate. Its concise syntax speeds up writing views significantly. However, its unique indentation-based structure can be a barrier for collaboration with non-coders. Best for projects where the dev team owns the entire view layer.
squirrelly is the modern contender offering the best of both worlds: the speed and safety of auto-escaping with the flexibility of optional logic. Its isomorphic nature makes it uniquely suited for modern stacks that share code between server and client. Consider it for new projects aiming for a lightweight, secure, and flexible architecture.
Choose pug if your priority is developer velocity and reducing HTML verbosity. Its whitespace-sensitive syntax eliminates closing tags and quotes, making it excellent for teams that value concise code. Avoid it if your designers or content editors need to read raw template files, as the indentation-based structure can be difficult for non-developers to parse.
Choose ejs if your team is already proficient in JavaScript and you need to embed complex logic directly into templates without learning a new DSL. It is ideal for legacy projects or scenarios where you need full access to JavaScript functions, but be prepared to manually sanitize outputs to prevent XSS vulnerabilities.
Choose handlebars if you want a balance between structure and extensibility. It enforces a logic-less view layer by default but allows you to register custom helpers for complex behavior. It is the standard choice for projects requiring a robust ecosystem of pre-built helpers and strong separation between data and presentation.
Choose mustache if you need a strictly logic-less template engine that guarantees portability across different languages (polyglot environments). It is best for simple rendering tasks where the template must remain purely presentational, and you do not need custom helpers or block iterators beyond the spec.
Choose squirrelly if you need a modern, secure engine that works seamlessly in both Node.js and the browser without a build step. It is suitable for projects that want logic capabilities similar to EJS but with auto-escaping enabled by default and a smaller footprint, making it a strong candidate for isomorphic applications.
Full documentation is at pugjs.org
Pug is a high performance template engine heavily influenced by Haml and implemented with JavaScript for Node.js and browsers. For bug reports, feature requests and questions, open an issue. For discussion join the chat room.
You can test drive Pug online here.
This project was formerly known as "Jade." However, it has been revealed to us that "Jade" is a registered trademark, and as a result a rename is needed. After some discussion among the maintainers, "Pug" has been chosen as the new name for this project. The next major version will carry "pug" as the package name.
If your package or app currently uses jade, don't worry: we have secured permissions to continue to occupy that package name, although all new versions will be released under pug.
Before the renaming, we had already begun working on an incompatible Jade 2.0.0. We have then made it so that this new major version bump will coincide with the rename to Pug. Therefore, upgrading from Jade to Pug will be the same process as upgrading any other package with a major version bump. Currently, Pug 2.0.0 is still under beta stage, and there are several syntactic differences we have deprecated and removed. Such differences are documented at #2305.
The website and documentation for Pug are still being updated, but if you are new to Pug, you should get started with the new syntax and install the Pug package on npm.
via npm:
$ npm install pug
After installing the latest version of Node.js, install with:
$ npm install pug-cli -g
and run with
$ pug --help
Pug is a clean, whitespace sensitive syntax for writing html. Here is a simple example:
doctype html
html(lang="en")
head
title= pageTitle
script(type='text/javascript').
if (foo) bar(1 + 5)
body
h1 Pug - node template engine
#container.col
if youAreUsingPug
p You are amazing
else
p Get on it!
p.
Pug is a terse and simple templating language with a
strong focus on performance and powerful features.
becomes
<!DOCTYPE html>
<html lang="en">
<head>
<title>Pug</title>
<script type="text/javascript">
if (foo) bar(1 + 5)
</script>
</head>
<body>
<h1>Pug - node template engine</h1>
<div id="container" class="col">
<p>You are amazing</p>
<p>Pug is a terse and simple templating language with a strong focus on performance and powerful features.</p>
</div>
</body>
</html>
For full API, see pugjs.org/api/reference.html
var pug = require('pug');
// compile
var fn = pug.compile('string of pug', options);
var html = fn(locals);
// render
var html = pug.render('string of pug', merge(options, locals));
// renderFile
var html = pug.renderFile('filename.pug', merge(options, locals));
filename Used in exceptions, and required when using includescompileDebug When false no debug instrumentation is compiledpretty Add pretty-indentation whitespace to output (false by default)The latest version of pug can be download for the browser in standalone form from here. It only supports the very latest browsers though, and is a large file. It is recommended that you pre-compile your pug templates to JavaScript.
To compile a template for use on the client using the command line, do:
$ pug --client --no-debug filename.pug
which will produce filename.js containing the compiled template.
Tutorials:
Implementations in other languages:
Other:
MIT