High-performance Jinja2/Django template engine for Bun - 2-4x faster than Nunjucks
Installation • Quick Start • Hono/Elysia • Multi-Engine • Filters
| Feature | Binja | Other JS engines |
|---|---|---|
| Runtime Performance | ✅ 2-4x faster | ❌ |
| AOT Compilation | ✅ 160x faster | ❌ |
| Multi-Engine | ✅ Jinja2, Handlebars, Liquid, Twig | ❌ |
| Framework Adapters | ✅ Hono, Elysia | ❌ |
| Django DTL Compatible | ✅ 100% | ❌ Partial |
| Jinja2 Compatible | ✅ Full | |
| Template Inheritance | ✅ | |
| 84 Built-in Filters | ✅ | ❌ |
| 28 Built-in Tests | ✅ | ❌ |
| Debug Panel | ✅ | ❌ |
| CLI Tool | ✅ | |
| Autoescape by Default | ✅ | ❌ |
| TypeScript | ✅ Native | |
| Bun Optimized | ✅ | ❌ |
Tested on Mac Studio M1 Max, Bun 1.3.5.
| Mode | Function | Best For | vs Nunjucks |
|---|---|---|---|
| Runtime | render() |
Development | 2-4x faster |
| AOT | compile() |
Production | 160x faster |
| Benchmark | binja | Nunjucks | Speedup |
|---|---|---|---|
| Simple Template | 371K ops/s | 96K ops/s | 3.9x |
| Complex Template | 44K ops/s | 23K ops/s | 2.0x |
| Multiple Filters | 246K ops/s | 63K ops/s | 3.9x |
| Nested Loops | 76K ops/s | 26K ops/s | 3.0x |
| Conditionals | 84K ops/s | 25K ops/s | 3.4x |
| HTML Escaping | 985K ops/s | 242K ops/s | 4.1x |
| Large Dataset | 9.6K ops/s | 6.6K ops/s | 1.5x |
| Benchmark | binja AOT | binja Runtime | Speedup |
|---|---|---|---|
| Simple Template | 14.3M ops/s | 371K ops/s | 39x |
| Complex Template | 1.07M ops/s | 44K ops/s | 24x |
| Nested Loops | 1.75M ops/s | 76K ops/s | 23x |
bun add binjaimport { render } from 'binja'
// Simple rendering
const html = await render('Hello, {{ name }}!', { name: 'World' })
// Output: Hello, World!
// With filters
const html = await render('{{ title|upper|truncatechars:20 }}', {
title: 'Welcome to our amazing website'
})
// Output: WELCOME TO OUR AMAZI...import { Environment } from 'binja'
const env = new Environment({
templates: './templates', // Template directory
autoescape: true, // XSS protection (default: true)
})
// Load and render template file
const html = await env.render('pages/home.html', {
user: { name: 'John', email: 'john@example.com' },
items: ['Apple', 'Banana', 'Cherry']
})For production, use compile() for 160x faster rendering:
import { compile } from 'binja'
// Compile once at startup
const renderUser = compile('<h1>{{ name|upper }}</h1>')
// Use many times (sync, extremely fast!)
const html = renderUser({ name: 'john' })
// Output: <h1>JOHN</h1>Production example:
import { compile } from 'binja'
// Pre-compile all templates at server startup
const templates = {
home: compile(await Bun.file('./views/home.html').text()),
user: compile(await Bun.file('./views/user.html').text()),
}
// Rendering is now synchronous and extremely fast
app.get('/', () => templates.home({ title: 'Welcome' }))
app.get('/user/:id', ({ params }) => templates.user({ id: params.id })){{ user.name }}
{{ user.email|lower }}
{{ items.0 }}
{{ data['key'] }}{% if user.is_admin %}
<span class="badge">Admin</span>
{% elif user.is_staff %}
<span class="badge">Staff</span>
{% else %}
<span class="badge">User</span>
{% endif %}{% for item in items %}
<div class="{{ loop.first ? 'first' : '' }}">
{{ loop.index }}. {{ item.name }}
</div>
{% empty %}
<p>No items found.</p>
{% endfor %}| Variable | Description |
|---|---|
loop.index / forloop.counter |
Current iteration (1-indexed) |
loop.index0 / forloop.counter0 |
Current iteration (0-indexed) |
loop.first / forloop.first |
True if first iteration |
loop.last / forloop.last |
True if last iteration |
loop.length / forloop.length |
Total number of items |
loop.parent / forloop.parentloop |
Parent loop context |
base.html
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}Default Title{% endblock %}</title>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>page.html
{% extends "base.html" %}
{% block title %}My Page{% endblock %}
{% block content %}
<h1>Welcome!</h1>
<p>This is my page content.</p>
{% endblock %}{% include "components/header.html" %}
{% include "components/card.html" with title="Hello" %}{% set greeting = "Hello, " ~ user.name %}
{{ greeting }}
{% with total = price * quantity %}
Total: ${{ total }}
{% endwith %}binja includes 84 built-in filters covering both Jinja2 and Django Template Language.
| Filter | Description | Example |
|---|---|---|
upper |
Uppercase | {{ "hello"|upper }} → HELLO |
lower |
Lowercase | {{ "HELLO"|lower }} → hello |
capitalize |
First letter uppercase | {{ "hello"|capitalize }} → Hello |
capfirst |
First char uppercase | {{ "hello"|capfirst }} → Hello |
title |
Title case | {{ "hello world"|title }} → Hello World |
trim |
Strip whitespace | {{ " hi "|trim }} → hi |
striptags |
Remove HTML tags | {{ "<p>Hi</p>"|striptags }} → Hi |
slugify |
URL-friendly slug | {{ "Hello World!"|slugify }} → hello-world |
truncatechars |
Truncate to N chars | {{ "hello"|truncatechars:3 }} → hel... |
truncatewords |
Truncate to N words | {{ "a b c d"|truncatewords:2 }} → a b... |
truncatechars_html |
Truncate preserving HTML | {{ "<b>hi</b> world"|truncatechars_html:5 }} |
truncatewords_html |
Truncate words in HTML | {{ "<p>a b c</p>"|truncatewords_html:2 }} |
wordcount |
Count words | {{ "hello world"|wordcount }} → 2 |
wordwrap |
Wrap at N chars | {{ text|wordwrap:40 }} |
center |
Center in N chars | {{ "hi"|center:10 }} → hi |
ljust |
Left justify | {{ "hi"|ljust:10 }} → hi |
rjust |
Right justify | {{ "hi"|rjust:10 }} → hi |
cut |
Remove substring | {{ "hello"|cut:"l" }} → heo |
replace |
Replace substring | {{ "hello"|replace:"l","x" }} → hexxo |
indent |
Indent lines | {{ text|indent:4 }} |
linebreaks |
Newlines to <p>/<br> |
{{ text|linebreaks }} |
linebreaksbr |
Newlines to <br> |
{{ text|linebreaksbr }} |
linenumbers |
Add line numbers | {{ code|linenumbers }} |
addslashes |
Escape quotes | {{ "it's"|addslashes }} → it\'s |
format |
sprintf-style format | {{ "Hi %s"|format:name }} |
stringformat |
Python % format | {{ 5|stringformat:"03d" }} → 005 |
| Filter | Description | Example |
|---|---|---|
abs |
Absolute value | {{ -5|abs }} → 5 |
int |
Convert to integer | {{ "42"|int }} → 42 |
float |
Convert to float | {{ "3.14"|float }} → 3.14 |
round |
Round number | {{ 3.7|round }} → 4 |
add |
Add number | {{ 5|add:3 }} → 8 |
divisibleby |
Check divisibility | {{ 10|divisibleby:2 }} → true |
floatformat |
Format decimal places | {{ 3.14159|floatformat:2 }} → 3.14 |
filesizeformat |
Human file size | {{ 1048576|filesizeformat }} → 1.0 MB |
get_digit |
Get Nth digit | {{ 12345|get_digit:2 }} → 4 |
| Filter | Description | Example |
|---|---|---|
length |
List length | {{ items|length }} → 3 |
length_is |
Check length | {{ items|length_is:3 }} → true |
first |
First item | {{ items|first }} |
last |
Last item | {{ items|last }} |
join |
Join with separator | {{ items|join:", " }} → a, b, c |
slice |
Slice list | {{ items|slice:":2" }} |
reverse |
Reverse list | {{ items|reverse }} |
sort |
Sort list | {{ items|sort }} |
unique |
Remove duplicates | {{ items|unique }} |
batch |
Group into batches | {{ items|batch:2 }} |
columns |
Split into columns | {{ items|columns:3 }} |
dictsort |
Sort dict by key | {{ dict|dictsort }} |
dictsortreversed |
Sort dict reversed | {{ dict|dictsortreversed }} |
groupby |
Group by attribute | {{ items|groupby:"category" }} |
random |
Random item | {{ items|random }} |
list |
Convert to list | {{ value|list }} |
make_list |
String to char list | {{ "abc"|make_list }} → ['a','b','c'] |
map |
Map attribute | {{ items|map:"name" }} |
select |
Filter by test | {{ items|select:"even" }} |
reject |
Reject by test | {{ items|reject:"none" }} |
selectattr |
Filter by attr test | {{ items|selectattr:"active" }} |
rejectattr |
Reject by attr test | {{ items|rejectattr:"hidden" }} |
| Filter | Description | Example |
|---|---|---|
max |
Maximum value | {{ items|max }} |
min |
Minimum value | {{ items|min }} |
sum |
Sum of values | {{ items|sum }} |
attr |
Get attribute | {{ item|attr:"name" }} |
| Filter | Description | Example |
|---|---|---|
date |
Format date | {{ now|date:"Y-m-d" }} → 2024-01-15 |
time |
Format time | {{ now|time:"H:i" }} → 14:30 |
timesince |
Time since date | {{ past|timesince }} → 2 days ago |
timeuntil |
Time until date | {{ future|timeuntil }} → in 3 hours |
const env = new Environment({
timezone: 'Europe/Rome' // All dates in Rome timezone
})| Filter | Description | Example |
|---|---|---|
escape / e |
HTML escape | {{ html|escape }} |
forceescape |
Force HTML escape | {{ html|forceescape }} |
safe |
Mark as safe | {{ html|safe }} |
safeseq |
Mark sequence safe | {{ items|safeseq }} |
escapejs |
JS string escape | {{ text|escapejs }} |
urlencode |
URL encode | {{ url|urlencode }} |
iriencode |
IRI encode | {{ url|iriencode }} |
urlize |
URLs to links | {{ text|urlize }} |
urlizetrunc |
URLs to links (truncated) | {{ text|urlizetrunc:15 }} |
json / tojson |
JSON stringify | {{ data|json }} |
json_script |
Safe JSON in script | {{ data|json_script:"id" }} |
pprint |
Pretty print | {{ data|pprint }} |
xmlattr |
Dict to XML attrs | {{ attrs|xmlattr }} |
| Filter | Description | Example |
|---|---|---|
default / d |
Default value | {{ missing|default:"N/A" }} |
default_if_none |
Default if null | {{ val|default_if_none:"None" }} |
yesno |
Boolean to text | {{ true|yesno:"Yes,No" }} → Yes |
pluralize |
Pluralize suffix | {{ count|pluralize }} → s |
| Filter | Description | Example |
|---|---|---|
items |
Dict to pairs | {% for k,v in dict|items %} |
unordered_list |
Nested list to HTML | {{ items|unordered_list }} |
Tests check values using the is operator (Jinja2 syntax):
{% if value is defined %}...{% endif %}
{% if num is even %}...{% endif %}
{% if num is divisibleby(3) %}...{% endif %}
{% if items is empty %}...{% endif %}| Test | Description |
|---|---|
divisibleby(n) |
Divisible by n |
even / odd |
Even/odd integer |
number / integer / float |
Type checks |
defined / undefined |
Variable exists |
none |
Is null |
empty |
Empty array/string/object |
truthy / falsy |
Truthiness checks |
string / mapping / iterable |
Type checks |
gt(n) / lt(n) / ge(n) / le(n) |
Comparisons |
eq(v) / ne(v) / sameas(v) |
Equality |
upper / lower |
String case checks |
import { builtinTests } from 'binja'
// All 28 built-in tests
console.log(Object.keys(builtinTests))
// ['divisibleby', 'even', 'odd', 'number', 'integer', 'float',
// 'defined', 'undefined', 'none', 'boolean', 'string', 'mapping',
// 'iterable', 'sequence', 'callable', 'upper', 'lower', 'empty',
// 'in', 'eq', 'ne', 'sameas', 'equalto', 'truthy', 'falsy', ...]Binja supports multiple template engines through a unified API. All engines parse to a common AST and share the same runtime, filters, and optimizations.
| Engine | Syntax | Use Case |
|---|---|---|
| Jinja2/DTL | {{ var }} {% if %} |
Default, Python/Django compatibility |
| Handlebars | {{var}} {{#if}} |
JavaScript ecosystem, Ember.js |
| Liquid | {{ var }} {% if %} |
Shopify, Jekyll, static sites |
| Twig | {{ var }} {% if %} |
PHP/Symfony, Drupal, Craft CMS |
// Direct engine imports
import * as handlebars from 'binja/engines/handlebars'
import * as liquid from 'binja/engines/liquid'
import * as twig from 'binja/engines/twig'
// Handlebars
await handlebars.render('Hello {{name}}!', { name: 'World' })
await handlebars.render('{{#each items}}{{this}}{{/each}}', { items: ['a', 'b'] })
await handlebars.render('{{{html}}}', { html: '<b>unescaped</b>' })
// Liquid (Shopify)
await liquid.render('Hello {{ name }}!', { name: 'World' })
await liquid.render('{% for item in items %}{{ item }}{% endfor %}', { items: ['a', 'b'] })
await liquid.render('{% assign x = "value" %}{{ x }}', {})
// Twig (Symfony)
await twig.render('Hello {{ name }}!', { name: 'World' })
await twig.render('{% for item in items %}{{ item }}{% endfor %}', { items: ['a', 'b'] })
await twig.render('{{ name|upper }}', { name: 'world' })import { MultiEngine } from 'binja/engines'
const engine = new MultiEngine()
// Render with any engine
await engine.render('Hello {{name}}!', { name: 'World' }, 'handlebars')
await engine.render('Hello {{ name }}!', { name: 'World' }, 'liquid')
await engine.render('Hello {{ name }}!', { name: 'World' }, 'twig')
await engine.render('Hello {{ name }}!', { name: 'World' }, 'jinja2')
// Auto-detect from file extension
import { detectEngine } from 'binja/engines'
const eng = detectEngine('template.hbs') // Returns Handlebars engine
const eng2 = detectEngine('page.liquid') // Returns Liquid engine
const eng3 = detectEngine('page.twig') // Returns Twig engine| Feature | Jinja2 | Handlebars | Liquid | Twig |
|---|---|---|---|---|
| Variables | {{ x }} |
{{x}} |
{{ x }} |
{{ x }} |
| Conditionals | {% if %} |
{{#if}} |
{% if %} |
{% if %} |
| Loops | {% for %} |
{{#each}} |
{% for %} |
{% for %} |
| Filters | {{ x|filter }} |
{{ x }} |
{{ x | filter }} |
{{ x|filter }} |
| Raw output | {% raw %} |
- | {% raw %} |
{% raw %} |
| Comments | {# #} |
{{! }} |
{% comment %} |
{# #} |
| Assignment | {% set %} |
- | {% assign %} |
{% set %} |
| Unescaped | {{ x|safe }} |
{{{x}}} |
- | {{ x|raw }} |
Binja provides first-class integration with Bun's most popular web frameworks.
import { Hono } from 'hono'
import { binja } from 'binja/hono'
const app = new Hono()
// Add binja middleware
app.use(binja({
root: './views', // Template directory
extension: '.html', // Default extension
engine: 'jinja2', // jinja2 | handlebars | liquid | twig
cache: true, // Cache compiled templates
globals: { siteName: 'My App' }, // Global context
layout: 'layouts/base', // Optional layout template
}))
// Render templates with c.render()
app.get('/', (c) => c.render('index', { title: 'Home' }))
app.get('/users/:id', async (c) => {
const user = await getUser(c.req.param('id'))
return c.render('users/profile', { user })
})
export default appimport { Elysia } from 'elysia'
import { binja } from 'binja/elysia'
const app = new Elysia()
// Add binja plugin
.use(binja({
root: './views',
extension: '.html',
engine: 'jinja2',
cache: true,
globals: { siteName: 'My App' },
layout: 'layouts/base',
}))
// Render templates with render()
.get('/', ({ render }) => render('index', { title: 'Home' }))
.get('/users/:id', async ({ render, params }) => {
const user = await getUser(params.id)
return render('users/profile', { user })
})
.listen(3000)
console.log('Server running at http://localhost:3000')| Option | Type | Default | Description |
|---|---|---|---|
root |
string |
./views |
Template directory |
extension |
string |
.html |
Default file extension |
engine |
string |
jinja2 |
Template engine (jinja2, handlebars, liquid, twig) |
cache |
boolean |
true (prod) |
Cache compiled templates |
debug |
boolean |
false |
Show error details |
globals |
object |
{} |
Global context variables |
layout |
string |
- | Layout template path |
contentVar |
string |
content |
Content variable name in layout |
import { clearCache, getCacheStats } from 'binja/hono'
// or
import { clearCache, getCacheStats } from 'binja/elysia'
// Clear all cached templates
clearCache()
// Get cache statistics
const stats = getCacheStats()
console.log(stats) // { size: 10, keys: ['jinja2:./views/index.html', ...] }binja is designed to be a drop-in replacement for Django templates:
{# Django-style comments #}
{% load static %} {# Supported (no-op) #}
{% url 'home' %}
{% static 'css/style.css' %}
{% csrf_token %}
{{ forloop.counter }}
{{ forloop.first }}
{{ forloop.parentloop.counter }}| Tag | Description | Example |
|---|---|---|
{% csrf_token %} |
CSRF token input | <input type="hidden" ...> |
{% cycle %} |
Cycle through values | {% cycle 'odd' 'even' %} |
{% firstof %} |
First truthy value | {% firstof var1 var2 "default" %} |
{% ifchanged %} |
Output on change | {% ifchanged %}{{ item }}{% endifchanged %} |
{% ifequal %} |
Equality check | {% ifequal a b %}equal{% endifequal %} |
{% lorem %} |
Lorem ipsum text | {% lorem 3 p %} |
{% regroup %} |
Group list by attr | {% regroup list by attr as grouped %} |
{% templatetag %} |
Literal tag chars | {% templatetag openblock %} → {% |
{% widthratio %} |
Calculate ratio | {% widthratio value max 100 %} |
{% debug %} |
Debug context | Outputs context as JSON |
const env = new Environment({
// Template directory
templates: './templates',
// Auto-escape HTML (default: true)
autoescape: true,
// Cache settings
cache: true, // Enable template caching (default: true)
cacheMaxSize: 100, // LRU cache limit (default: 100)
// Timezone for date/time operations
// All date filters and {% now %} tag will use this timezone
timezone: 'Europe/Rome', // or 'UTC', 'America/New_York', etc.
// Custom filters
filters: {
currency: (value: number) => `$${value.toFixed(2)}`,
highlight: (text: string, term: string) =>
text.replace(new RegExp(term, 'gi'), '<mark>$&</mark>')
},
// Global variables available in all templates
globals: {
site_name: 'My Website',
current_year: new Date().getFullYear()
},
// URL resolver for {% url %} tag
urlResolver: (name: string, ...args: any[]) => {
const routes = { home: '/', about: '/about', user: '/users/:id' }
return routes[name] || '#'
},
// Static file resolver for {% static %} tag
staticResolver: (path: string) => `/static/${path}`
})
// Cache monitoring
env.cacheSize() // Number of cached templates
env.cacheStats() // { size, maxSize, hits, misses, hitRate }
env.clearCache() // Clear cache and reset statsBinja includes a professional debug panel for development, similar to Django Debug Toolbar:
const env = new Environment({
templates: './templates',
debug: true, // Enable debug panel
debugOptions: {
dark: true,
position: 'bottom-right',
},
})
// Debug panel is automatically injected into HTML responses
const html = await env.render('page.html', context)- Performance Metrics - Lexer, Parser, Render timing with visual bars
- Template Chain - See extends/include hierarchy
- Context Inspector - Expandable tree view of all context variables
- Filter Usage - Which filters were used and how many times
- Cache Stats - Hit/miss rates
- Warnings - Optimization suggestions
debugOptions: {
dark: true, // Dark/light theme
collapsed: true, // Start collapsed
position: 'bottom-right', // Panel position
width: 420, // Panel width
}Binja includes a CLI for template pre-compilation and linting:
# Compile all templates to JavaScript
binja compile ./templates -o ./dist
# Check templates for errors
binja check ./templates
# Watch mode for development
binja watch ./templates -o ./dist
# Lint templates (syntax check)
binja lint ./templates
# Lint with AI analysis (requires API key)
binja lint ./templates --ai
# Lint with specific AI provider
binja lint ./templates --ai=ollama// Generated: dist/home.js
import { render } from './dist/home.js'
const html = render({ title: 'Home', items: [...] })Binja includes an optional AI-powered linting module that detects security issues, performance problems, accessibility concerns, and best practice violations.
The AI module is opt-in. Install the SDK for your preferred provider:
# For Claude (Anthropic)
bun add @anthropic-ai/sdk
# For OpenAI
bun add openai
# For Ollama (local) - no package needed
# For Groq - no package neededSet the API key for your provider:
# Anthropic
export ANTHROPIC_API_KEY=sk-ant-...
# OpenAI
export OPENAI_API_KEY=sk-...
# Groq (free tier available)
export GROQ_API_KEY=gsk_...
# Ollama - no key needed, just run: ollama serve# Lint with AI (auto-detect provider)
binja lint ./templates --ai
# Use specific provider
binja lint ./templates --ai=anthropic
binja lint ./templates --ai=openai
binja lint ./templates --ai=ollama
binja lint ./templates --ai=groq
# JSON output for CI/CD
binja lint ./templates --ai --format=jsonimport { lint } from 'binja/ai'
// Auto-detect provider from environment
const result = await lint(template)
// Specify provider and API key directly
const result = await lint(template, {
provider: 'anthropic',
apiKey: 'sk-ant-...',
model: 'claude-sonnet-4-20250514'
})
// Check results
console.log(result.errors) // Syntax errors
console.log(result.warnings) // Security, performance issues
console.log(result.suggestions) // Best practice recommendations
console.log(result.provider) // Which AI was used| Category | Examples |
|---|---|
| Security | XSS vulnerabilities, |safe on user input, sensitive data exposure |
| Performance | Heavy filters in loops, repeated calculations |
| Accessibility | Missing alt text, forms without labels |
| Best Practices | {% for %} without {% empty %}, deep nesting |
| Provider | API Key | Speed | Cost |
|---|---|---|---|
| Anthropic | ANTHROPIC_API_KEY |
Fast | Paid |
| OpenAI | OPENAI_API_KEY |
Fast | Paid |
| Groq | GROQ_API_KEY |
Very Fast | Free tier |
| Ollama | None (local) | Varies | Free |
Auto-detect priority: Anthropic → OpenAI → Groq → Ollama
Output template syntax without processing:
{% raw %}
{{ this will not be processed }}
{% neither will this %}
{% endraw %}
{# Or Django-style #}
{% verbatim %}
{{ raw output }}
{% endverbatim %}const env = new Environment({
filters: {
// Simple filter
double: (value: number) => value * 2,
// Filter with argument
repeat: (value: string, times: number = 2) => value.repeat(times),
// Async filter
translate: async (value: string, lang: string) => {
return await translateAPI(value, lang)
}
}
})Usage:
{{ 5|double }} → 10
{{ "hi"|repeat:3 }} → hihihi
{{ "Hello"|translate:"es" }} → HolaAutoescape is enabled by default. All variables are HTML-escaped:
await render('{{ script }}', {
script: '<script>alert("xss")</script>'
})
// Output: <script>alert("xss")</script>{{ trusted_html|safe }}- Use AOT in Production -
compile()is 160x faster than Nunjucks - Pre-compile at Startup - Compile templates once, use many times
- Reuse Environment - For templates with
{% extends %}, create once - LRU Cache - Templates cached with LRU eviction (default: 100, prevents memory leaks)
- Monitor Cache - Use
env.cacheStats()to optimizecacheMaxSize
import { compile } from 'binja'
// Best: AOT compilation for static templates
const templates = {
home: compile(await Bun.file('./views/home.html').text()),
user: compile(await Bun.file('./views/user.html').text()),
}
// Sync rendering, extremely fast
app.get('/', () => templates.home({ title: 'Home' }))
app.get('/user/:id', () => templates.user({ id: params.id }))For templates with inheritance ({% extends %}):
import { Environment } from 'binja'
// Environment with cache for inherited templates
const env = new Environment({ templates: './views', cache: true })
// Pre-warm cache at startup
await env.loadTemplate('base.html')
await env.loadTemplate('home.html')Render a template string with context (async, easy development).
import { render } from 'binja'
const html = await render('Hello {{ name }}', { name: 'World' })Compile a template to an optimized function (sync, 160x faster).
import { compile } from 'binja'
// Compile once
const renderGreeting = compile('<h1>{{ name|upper }}</h1>')
// Use many times (sync!)
const html = renderGreeting({ name: 'world' }) // <h1>WORLD</h1>Supported: Variables, filters, conditions, loops, set/with, comments.
Not supported: {% extends %}, {% include %} (use Environment for these).
Generate JavaScript code string for build tools.
import { compileToCode } from 'binja'
const code = compileToCode('<h1>{{ title }}</h1>', {
functionName: 'renderHeader'
})
// Save to file for bundling
await Bun.write('./compiled/header.js', code)Create a configured template environment.
const env = new Environment(options)
// Rendering
env.render(name, context) // Render template file
env.renderString(str, context) // Render template string
// Configuration
env.addFilter(name, fn) // Add custom filter
env.addGlobal(name, value) // Add global variable
// Cache Management (LRU with configurable max size)
env.loadTemplate(name) // Pre-load template (cache warming)
env.cacheSize() // Get number of cached templates
env.cacheStats() // Get { size, maxSize, hits, misses, hitRate }
env.clearCache() // Clear all cached templates and reset statsimport { Elysia } from 'elysia'
import { Environment } from 'binja'
// Development with debug panel
const templates = new Environment({
templates: './views',
debug: Bun.env.NODE_ENV !== 'production',
debugOptions: { dark: true },
globals: {
site_name: 'My App',
current_year: new Date().getFullYear()
}
})
const app = new Elysia()
// HTML helper
.decorate('html', (name: string, ctx: object) => templates.render(name, ctx))
// Routes
.get('/', async ({ html }) => {
return new Response(await html('home.html', {
title: 'Welcome',
features: ['Fast', 'Secure', 'Easy']
}), {
headers: { 'Content-Type': 'text/html' }
})
})
.get('/users/:id', async ({ html, params }) => {
const user = await getUser(params.id)
return new Response(await html('user/profile.html', { user }), {
headers: { 'Content-Type': 'text/html' }
})
})
.listen(3000)
console.log('Server running at http://localhost:3000')import { Elysia } from 'elysia'
import { Environment } from 'binja'
// Create reusable plugin
const jinjaPlugin = (options: { templates: string }) => {
const env = new Environment(options)
return new Elysia({ name: 'jinja' })
.derive(async () => ({
render: async (name: string, context: object = {}) => {
const html = await env.render(name, context)
return new Response(html, {
headers: { 'Content-Type': 'text/html; charset=utf-8' }
})
}
}))
}
// Use in app
const app = new Elysia()
.use(jinjaPlugin({ templates: './views' }))
.get('/', ({ render }) => render('index.html', { title: 'Home' }))
.get('/about', ({ render }) => render('about.html'))
.listen(3000)import { Elysia } from 'elysia'
import { Environment } from 'binja'
const templates = new Environment({ templates: './views' })
const app = new Elysia()
// Full page
.get('/', async () => {
const html = await templates.render('index.html', {
items: await getItems()
})
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
})
})
// HTMX partial - returns only the component
.post('/items', async ({ body }) => {
const item = await createItem(body)
const html = await templates.renderString(`
<li id="item-{{ item.id }}" class="item">
{{ item.name }}
<button hx-delete="/items/{{ item.id }}" hx-target="#item-{{ item.id }}" hx-swap="outerHTML">
Delete
</button>
</li>
`, { item })
return new Response(html, {
headers: { 'Content-Type': 'text/html' }
})
})
.delete('/items/:id', async ({ params }) => {
await deleteItem(params.id)
return new Response('', { status: 200 })
})
.listen(3000)import { Hono } from 'hono'
import { Environment } from 'binja'
const app = new Hono()
// Development with debug panel
const templates = new Environment({
templates: './views',
debug: process.env.NODE_ENV !== 'production',
debugOptions: { dark: true, position: 'bottom-right' }
})
app.get('/', async (c) => {
const html = await templates.render('index.html', {
title: 'Home',
user: c.get('user')
})
return c.html(html)
})
app.get('/products', async (c) => {
const products = await getProducts()
return c.html(await templates.render('products/list.html', { products }))
})const env = new Environment({ templates: './emails' })
const html = await env.render('welcome.html', {
user: { name: 'John', email: 'john@example.com' },
activation_link: 'https://example.com/activate/xyz'
})
await sendEmail({
to: user.email,
subject: 'Welcome!',
html
})import { Environment } from 'binja'
const templates = new Environment({ templates: './templates' })
// Render invoice HTML
const html = await templates.render('invoice.html', {
invoice: {
number: 'INV-2024-001',
date: new Date(),
customer: { name: 'Acme Corp', address: '123 Main St' },
items: [
{ name: 'Service A', qty: 2, price: 100 },
{ name: 'Service B', qty: 1, price: 250 }
],
total: 450
}
})
// Use with any PDF library (puppeteer, playwright, etc.)
const pdf = await generatePDF(html)import { Environment } from 'binja'
import { readdir, writeFile, mkdir } from 'fs/promises'
const env = new Environment({ templates: './src/templates' })
// Build all pages
const pages = [
{ template: 'index.html', output: 'dist/index.html', data: { title: 'Home' } },
{ template: 'about.html', output: 'dist/about.html', data: { title: 'About' } },
{ template: 'contact.html', output: 'dist/contact.html', data: { title: 'Contact' } }
]
await mkdir('dist', { recursive: true })
for (const page of pages) {
const html = await env.render(page.template, page.data)
await writeFile(page.output, html)
console.log(`Built: ${page.output}`)
}binja is inspired by and aims to be compatible with:
- Jinja2 - The original Python template engine by Pallets Projects (BSD-3-Clause)
- Django Template Language - Django's built-in template system (BSD-3-Clause)
BSD-3-Clause
See LICENSE for details.
Made with ❤️ for the Bun ecosystem