npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nextrush/template

v3.0.5

Published

Modern template engine for NextRush - Mustache-like syntax with helpers, partials, layouts, and streaming

Readme

@nextrush/template

Modern, secure template engine for NextRush with Mustache-like syntax, 70+ helpers, partials, layouts, and multi-engine support.

Highlights

  • 🔒 Security First - Blocks prototype pollution, path traversal, XSS, and infinite recursion
  • 🔌 Multi-Engine Support - Use EJS, Handlebars, Nunjucks, Pug, Eta, or the built-in engine
  • 🎯 Simple One-Liner Setup - Get started with just app.use(template())
  • 🚀 Zero Dependencies - Built-in engine requires no external dependencies
  • ⚡ Production Ready - Automatic caching, layouts, and 70+ helpers
  • 📝 Express Compatible - Familiar ctx.render() API

Installation

pnpm add @nextrush/template

Optional Engines

Install only the engines you need:

pnpm add ejs          # For EJS
pnpm add handlebars   # For Handlebars
pnpm add nunjucks     # For Nunjucks
pnpm add pug          # For Pug
pnpm add eta          # For Eta (modern EJS)

Quick Start

import { createApp } from '@nextrush/core';
import { template } from '@nextrush/template';

const app = createApp();

// Simplest setup - uses built-in engine
app.use(template());

// With views directory
app.use(template({ root: './views' }));

app.get('/', async (ctx) => {
  await ctx.render('home', { title: 'Welcome' });
});

Using Different Engines

EJS

import { template } from '@nextrush/template';

// npm install ejs
app.use(template('ejs', { root: './views' }));

app.get('/', async (ctx) => {
  await ctx.render('home', { name: 'World' });
});

views/home.ejs:

<h1>Hello <%= name %>!</h1>

Handlebars

// npm install handlebars
app.use(
  template('handlebars', {
    root: './views',
    ext: '.hbs',
    layout: 'layouts/main',
  })
);

views/home.hbs:

<h1>Hello {{name}}!</h1>

Nunjucks

// npm install nunjucks
app.use(
  template('nunjucks', {
    root: './views',
    autoescape: true,
  })
);

views/home.njk:

<h1>Hello {{ name }}!</h1>

Pug

// npm install pug
app.use(template('pug', { root: './views', pretty: true }));

views/home.pug:

h1 Hello #{name}!

Eta (Modern EJS)

// npm install eta
app.use(template('eta', { root: './views', autoEscape: true }));

views/home.eta:

<h1>Hello <%= it.name %>!</h1>

Built-in Engine (Default)

The built-in engine uses Mustache-like syntax with no dependencies:

app.use(template({ root: './views' }));

views/home.html:

<h1>Hello {{name}}!</h1>

Configuration Options

Common Options

| Option | Type | Default | Description | | --------- | --------- | -------------------- | ---------------------------------------------------------------- | | root | string | './views' | Template directory | | ext | string | varies | File extension (.ejs, .hbs, .njk, .pug, .eta, .html) | | cache | boolean | true in production | Enable template caching | | layout | string | - | Default layout template | | helpers | object | - | Custom helper functions |

Engine-Specific Options

EJS

  • delimiter - Custom delimiter (default: %)
  • openDelimiter - Opening delimiter
  • closeDelimiter - Closing delimiter

Handlebars

  • strict - Enable strict mode
  • preventIndent - Prevent partial indentation

Nunjucks

  • autoescape - Enable auto-escaping (default: true)
  • throwOnUndefined - Throw on undefined variables
  • watch - Watch for file changes (development)

Pug

  • pretty - Pretty print output
  • doctype - HTML doctype

Eta

  • autoEscape - Enable auto-escaping (default: true)
  • autoTrim - Trim whitespace

Features

Layouts

app.use(
  template('handlebars', {
    root: './views',
    layout: 'layouts/main',
  })
);

views/layouts/main.hbs:

<!DOCTYPE html>
<html>
<head><title>{{title}}</title></head>
<body>
  {{{body}}}
</body>
</html>

views/home.hbs:

<h1>{{title}}</h1>
<p>Welcome to our site!</p>

Custom Helpers

app.use(
  template({
    helpers: {
      formatDate: (date) => new Date(date).toLocaleDateString(),
      currency: (value) => `$${Number(value).toFixed(2)}`,
    },
  })
);

Built-in template usage:

<p>Date: {{createdAt | formatDate}}</p>
<p>Price: {{price | currency}}</p>

Override Layout Per Request

app.get('/admin', async (ctx) => {
  await ctx.render('admin/dashboard', { user }, { layout: 'layouts/admin' });
});

app.get('/print', async (ctx) => {
  await ctx.render('report', { data }, { layout: null }); // No layout
});

Built-in Engine Features

The built-in Mustache-like engine includes:

String Helpers

| Helper | Usage | Description | | ------------ | --------------------------------- | ----------------------- | | upper | {{name \| upper}} | Convert to uppercase | | lower | {{name \| lower}} | Convert to lowercase | | capitalize | {{name \| capitalize}} | Capitalize first letter | | titleCase | {{name \| titleCase}} | Title Case String | | trim | {{text \| trim}} | Remove whitespace | | truncate | {{text \| truncate 100 "..."}} | Limit length | | replace | {{text \| replace "old" "new"}} | Replace substring | | padStart | {{num \| padStart 3 "0"}} | Pad start of string | | padEnd | {{num \| padEnd 3 "0"}} | Pad end of string | | stripHtml | {{html \| stripHtml}} | Remove HTML tags | | split | {{csv \| split ","}} | Split string to array | | join | {{arr \| join "-"}} | Join array to string | | reverse | {{text \| reverse}} | Reverse string | | length | {{text \| length}} | Get string/array length |

Number Helpers

| Helper | Usage | Description | | -------------- | --------------------------------- | ----------------------- | | round | {{num \| round 2}} | Round to decimal places | | floor | {{num \| floor}} | Round down | | ceil | {{num \| ceil}} | Round up | | abs | {{num \| abs}} | Absolute value | | add | {{num \| add 10}} | Add numbers | | subtract | {{num \| subtract 5}} | Subtract numbers | | multiply | {{num \| multiply 2}} | Multiply numbers | | divide | {{num \| divide 2}} | Divide numbers | | mod | {{num \| mod 3}} | Modulo operation | | formatNumber | {{num \| formatNumber "en-US"}} | Locale formatting | | currency | {{price \| currency "USD"}} | Currency formatting | | percent | {{ratio \| percent 2}} | Percentage formatting |

Array Helpers

| Helper | Usage | Description | | ---------- | ---------------------------- | --------------------- | | first | {{arr \| first}} | First element | | last | {{arr \| last}} | Last element | | at | {{arr \| at 2}} | Element at index | | slice | {{arr \| slice 1 3}} | Slice array | | sort | {{arr \| sort}} | Sort array | | unique | {{arr \| unique}} | Remove duplicates | | compact | {{arr \| compact}} | Remove falsy values | | flatten | {{arr \| flatten}} | Flatten nested arrays | | includes | {{arr \| includes "item"}} | Check if includes | | indexOf | {{arr \| indexOf "item"}} | Find index |

Object Helpers

| Helper | Usage | Description | | --------- | -------------------------------- | ------------------- | | keys | {{obj \| keys}} | Get object keys | | values | {{obj \| values}} | Get object values | | entries | {{obj \| entries}} | Get key-value pairs | | get | {{obj \| get "path.to.value"}} | Get nested value |

Comparison Helpers

| Helper | Usage | Description | | ------ | ---------------- | --------------------- | | eq | {{a \| eq b}} | Equal | | ne | {{a \| ne b}} | Not equal | | lt | {{a \| lt b}} | Less than | | lte | {{a \| lte b}} | Less than or equal | | gt | {{a \| gt b}} | Greater than | | gte | {{a \| gte b}} | Greater than or equal | | and | {{a \| and b}} | Logical AND | | or | {{a \| or b}} | Logical OR | | not | {{val \| not}} | Logical NOT |

Type Helpers

| Helper | Usage | Description | | ---------- | --------------------- | --------------- | | isArray | {{val \| isArray}} | Check if array | | isObject | {{val \| isObject}} | Check if object | | isString | {{val \| isString}} | Check if string | | isNumber | {{val \| isNumber}} | Check if number | | isEmpty | {{val \| isEmpty}} | Check if empty |

Output Helpers

| Helper | Usage | Description | | --------- | ------------------------------- | ------------------------ | | json | {{{data \| json}}} | JSON stringify | | safe | {{html \| safe}} | Mark as safe (no escape) | | default | {{val \| default "fallback"}} | Default value | | if | {{active \| if "Yes" "No"}} | Inline conditional |

API Reference

compile(source, options?)

Compile a template string into a reusable template object.

import { compile } from '@nextrush/template';

const template = compile('Hello {{name}}!', {
  escape: true, // HTML escape by default (default: true)
  strict: false, // Throw on missing variables (default: false)
  delimiters: ['{{', '}}'], // Custom delimiters
  helpers: {
    // Custom helpers
    double: (v) => Number(v) * 2,
  },
});

// Synchronous render
const result = template.render({ name: 'World' });

// Async render (supports async helpers)
const asyncResult = await template.renderAsync({ name: 'World' });

parse(source, options?)

Parse a template string into an AST.

import { parse } from '@nextrush/template';

const ast = parse('Hello {{name}}!');
// Returns AST with body array of nodes

validate(source, options?)

Validate a template string without compiling.

import { validate } from '@nextrush/template';

const isValid = validate('Hello {{name}}!'); // true
validate('Hello {{name'); // throws TemplateParseError

createEngine(options?)

Create a template engine for file-based templates.

import { createEngine } from '@nextrush/template';

const engine = createEngine({
  root: './views', // Template root directory
  ext: '.hbs', // File extension
  cache: true, // Enable caching (default: true)
  layout: 'layouts/main', // Default layout
  helpers: {}, // Global helpers
  partials: {}, // Global partials
});

// Render a template file
const html = await engine.render('pages/home', { title: 'Home' });

// Render a string template
const result = await engine.renderString('Hello {{name}}!', { name: 'World' });

// Register helpers
engine.registerHelper('shout', (v) => String(v).toUpperCase() + '!');

// Register partials
engine.registerPartial('header', '<header>{{title}}</header>');

// Clear cache
engine.clearCache();

Custom Helpers

Value Helpers (Pipe Helpers)

Value helpers receive the piped value and any additional arguments:

const template = compile('{{value | double}}', {
  helpers: {
    double: (value) => Number(value) * 2,
  },
});

// With arguments
const template2 = compile('{{value | multiply 3}}', {
  helpers: {
    multiply: (value, factor) => Number(value) * Number(factor),
  },
});

Async Helpers

const template = compile('{{userId | fetchUser | get "name"}}', {
  helpers: {
    fetchUser: async (id) => {
      const response = await fetch(`/api/users/${id}`);
      return response.json();
    },
  },
});

const result = await template.renderAsync({ userId: 123 });

NextRush Integration

Middleware

import { createApp } from '@nextrush/core';
import { template } from '@nextrush/template';

const app = createApp();

// Use template middleware
app.use(
  template({
    root: './views',
    ext: '.hbs',
    helpers: {
      formatDate: (d) => new Date(d).toLocaleDateString(),
    },
  })
);

// In route handlers
app.get('/', async (ctx) => {
  await ctx.render('home', {
    title: 'Welcome',
    user: ctx.state.user,
  });
});

Plugin

import { createApp } from '@nextrush/core';
import { templatePlugin } from '@nextrush/template';

const app = createApp();

app.plugin(
  templatePlugin({
    root: './views',
    ext: '.hbs',
  })
);

Security

XSS Protection

By default, all variables are HTML-escaped to prevent XSS attacks:

const template = compile('{{comment}}');
template.render({ comment: '<script>alert("XSS")</script>' });
// => '&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;'

Use triple mustache {{{raw}}} or {{& raw}} only for trusted content:

const template = compile('{{{trustedHtml}}}');
template.render({ trustedHtml: '<b>Bold</b>' });
// => '<b>Bold</b>'

Prototype Pollution Protection

Access to dangerous properties is blocked (based on CVE-2021-23369):

{{__proto__}}              <!-- Returns empty -->
{{constructor}}            <!-- Returns empty -->
{{obj.__proto__.polluted}} <!-- Returns empty -->
{{obj | get "constructor"}}<!-- Returns empty -->

Path Traversal Protection

Template file paths are validated to prevent reading arbitrary files:

await ctx.render('../../../etc/passwd', {});
// Throws: Path traversal detected: "../../../etc/passwd" contains ".." segments

await ctx.render('/etc/passwd', {});
// Throws: Path traversal detected: "/etc/passwd" resolves outside the views directory

Recursion Protection

Infinite partial and layout loops are prevented:

// If partial 'recursive' includes {{>recursive}}
await ctx.render('page', {}, { partials: { recursive: '{{>recursive}}' } });
// Throws: Maximum template nesting depth (100) exceeded

// If layout A includes layout B which includes layout A
await ctx.render('page', {}, { layout: 'A' });
// Throws: Maximum layout nesting depth (10) exceeded

Security Summary

| Vulnerability | Protection | | -------------------------- | ----------------------------------------------------------- | | XSS (Cross-Site Scripting) | HTML escaping enabled by default | | Prototype Pollution | Blocked properties: __proto__, constructor, prototype | | Path Traversal | Path validation, root directory enforcement | | DoS via Recursion | Max nesting depth: 100 for partials, 10 for layouts | | ReDoS | Safe regex patterns in parser |

Performance

The template engine is optimized for performance:

  • Compiled Templates: Templates are parsed once and reused
  • Efficient Rendering: Minimal allocations during rendering
  • Caching: Built-in template caching for file-based templates
  • Benchmarks: Handles 10,000+ items in under 500ms

Runtime Compatibility

| Runtime | String Rendering | File Rendering | | ------------------------ | ---------------- | ---------------------- | | Node.js 20+ | ✅ Full support | ✅ Full support | | Bun | ✅ Full support | ✅ Full support | | Deno | ✅ Full support | ✅ With --allow-read | | Edge (Cloudflare/Vercel) | ✅ Full support | ❌ No filesystem |

For edge runtimes without filesystem access, use string rendering:

import { compile } from '@nextrush/template';

// Pre-compile templates at build time
const homeTemplate = compile(`
  <h1>{{title}}</h1>
  <p>Welcome, {{user.name}}!</p>
`);

// Render at runtime (no filesystem needed)
export default {
  async fetch(request) {
    const html = homeTemplate.render({
      title: 'Welcome',
      user: { name: 'Edge User' },
    });
    return new Response(html, {
      headers: { 'Content-Type': 'text/html' },
    });
  },
};

Error Handling

import { compile, TemplateParseError } from '@nextrush/template';

try {
  compile('{{#if open}}content'); // Missing closing tag
} catch (error) {
  if (error instanceof TemplateParseError) {
    console.log(error.message); // "Unclosed block: if"
    console.log(error.line); // Line number
    console.log(error.column); // Column number
  }
}

TypeScript

Full TypeScript support with type definitions:

import type {
  CompiledTemplate,
  CompileOptions,
  RenderOptions,
  AST,
  HelperFn,
  ValueHelper,
} from '@nextrush/template';

// Custom helper type
const myHelper: ValueHelper = (value, ...args) => {
  return String(value).toUpperCase();
};

License

MIT © NextRush Team