@nextrush/template
v3.0.5
Published
Modern template engine for NextRush - Mustache-like syntax with helpers, partials, layouts, and streaming
Maintainers
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/templateOptional 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 delimitercloseDelimiter- Closing delimiter
Handlebars
strict- Enable strict modepreventIndent- Prevent partial indentation
Nunjucks
autoescape- Enable auto-escaping (default:true)throwOnUndefined- Throw on undefined variableswatch- Watch for file changes (development)
Pug
pretty- Pretty print outputdoctype- 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 nodesvalidate(source, options?)
Validate a template string without compiling.
import { validate } from '@nextrush/template';
const isValid = validate('Hello {{name}}!'); // true
validate('Hello {{name'); // throws TemplateParseErrorcreateEngine(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>' });
// => '<script>alert("XSS")</script>'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 directoryRecursion 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) exceededSecurity 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
