Handlebars
Build dynamic email templates with Handlebars and Keplars Mail Service
Handlebars is a simple templating language that lets you build dynamic emails with variables, conditionals, and loops. It uses mustache-style syntax and keeps your templates clean and logic-free.
Why Handlebars?
- Simple Syntax: Easy-to-learn mustache-style tags like
{{variable}} - Logic-Less Templates: Keeps presentation separate from business logic
- Built-in Helpers: Conditionals, loops, and custom helpers
- Partials Support: Reusable template components
- Wide Adoption: Works with Nodemailer and most Node.js email libraries
Installation
Install Handlebars in your Node.js project:
npm install handlebarsFor email-specific features with Nodemailer:
npm install nodemailer nodemailer-express-handlebars handlebarsBasic Syntax
Variables
Hello {{name}}!
Your order #{{orderId}} has been confirmed.Conditionals
{{#if isPremium}}
<p>Thank you for being a premium member!</p>
{{else}}
<p>Upgrade to premium for more features.</p>
{{/if}}Loops
<ul>
{{#each items}}
<li>{{this.name}} - ${{this.price}}</li>
{{/each}}
</ul>Nested Objects
{{user.name}}
{{user.email}}
{{user.address.city}}Basic Example
Create a simple welcome email template:
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.button { background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; }
</style>
</head>
<body>
<div class="container">
<h1>Welcome {{userName}}!</h1>
<p>Thanks for signing up on {{signupDate}}.</p>
<p>
<a href="{{dashboardUrl}}" class="button">Go to Dashboard</a>
</p>
</div>
</body>
</html>Using with Node.js
Compile Handlebars templates and send with Keplars Mail Service:
import Handlebars from 'handlebars';
const templateSource = `
<!DOCTYPE html>
<html>
<body>
<h1>Hello {{name}}!</h1>
<p>Your account balance is ${{balance}}.</p>
{{#if isPremium}}
<p>Premium features are now available.</p>
{{/if}}
</body>
</html>
`;
// Compile the template
const template = Handlebars.compile(templateSource);
// Render with data
const html = template({
name: 'John',
balance: 150.50,
isPremium: true
});
// Send with Keplars Mail Service
await fetch('https://api.keplars.com/api/v1/send-email/async', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.KEPLERS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: ['[email protected]'],
subject: 'Account Update',
body: html
})
});Using with Template Files
Store templates as .hbs files and load them dynamically:
import { readFile } from 'fs/promises';
import Handlebars from 'handlebars';
async function sendOrderConfirmation(orderData: any) {
// Load template file
const templateSource = await readFile('./templates/order-confirmation.hbs', 'utf-8');
// Compile template
const template = Handlebars.compile(templateSource);
// Render with data
const html = template(orderData);
// Send email
const response = await fetch('https://api.keplars.com/api/v1/send-email/async', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.KEPLERS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: [orderData.email],
subject: `Order #${orderData.orderId} Confirmed`,
body: html
})
});
return response.json();
}
// Usage
await sendOrderConfirmation({
email: '[email protected]',
orderId: '12345',
customerName: 'John Doe',
items: [
{ name: 'Product A', price: 29.99, quantity: 2 },
{ name: 'Product B', price: 49.99, quantity: 1 }
],
total: 109.97
});Template File Example
Create templates/order-confirmation.hbs:
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
table { width: 100%; border-collapse: collapse; }
th, td { padding: 10px; text-align: left; border-bottom: 1px solid #ddd; }
.total { font-weight: bold; font-size: 18px; }
</style>
</head>
<body>
<div class="container">
<h1>Order Confirmation</h1>
<p>Hi {{customerName}},</p>
<p>Thank you for your order! Your order #{{orderId}} has been confirmed.</p>
<h2>Order Details</h2>
<table>
<thead>
<tr>
<th>Item</th>
<th>Quantity</th>
<th>Price</th>
</tr>
</thead>
<tbody>
{{#each items}}
<tr>
<td>{{this.name}}</td>
<td>{{this.quantity}}</td>
<td>${{this.price}}</td>
</tr>
{{/each}}
</tbody>
</table>
<p class="total">Total: ${{total}}</p>
<p>We'll send you a shipping confirmation when your items are on the way.</p>
</div>
</body>
</html>Express.js Integration
Create an email service with Handlebars:
import express from 'express';
import Handlebars from 'handlebars';
import { readFile } from 'fs/promises';
const app = express();
app.use(express.json());
// Cache compiled templates
const templates = new Map<string, HandlebarsTemplateDelegate>();
async function getTemplate(name: string) {
if (!templates.has(name)) {
const source = await readFile(`./templates/${name}.hbs`, 'utf-8');
templates.set(name, Handlebars.compile(source));
}
return templates.get(name)!;
}
app.post('/send-welcome-email', async (req, res) => {
const { email, name } = req.body;
const template = await getTemplate('welcome');
const html = template({
name,
dashboardUrl: 'https://example.com/dashboard',
supportEmail: '[email protected]'
});
const response = await fetch('https://api.keplars.com/api/v1/send-email/async', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.KEPLERS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: [email],
subject: 'Welcome!',
body: html
})
});
const result = await response.json();
res.json({ success: true, messageId: result.messageId });
});Using with Nodemailer
Handlebars works seamlessly with Nodemailer:
import nodemailer from 'nodemailer';
import hbs from 'nodemailer-express-handlebars';
import path from 'path';
// Configure Nodemailer to use Keplars SMTP (if using SMTP)
const transporter = nodemailer.createTransport({
host: 'smtp.keplars.com',
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASSWORD
}
});
// Configure Handlebars
transporter.use('compile', hbs({
viewEngine: {
extname: '.hbs',
partialsDir: path.resolve('./templates/partials'),
defaultLayout: false,
},
viewPath: path.resolve('./templates'),
extName: '.hbs',
}));
// Send email
await transporter.sendMail({
from: '[email protected]',
to: '[email protected]',
subject: 'Welcome!',
template: 'welcome', // loads welcome.hbs
context: {
name: 'John',
dashboardUrl: 'https://example.com/dashboard'
}
});Custom Helpers
Create custom Handlebars helpers for common operations:
import Handlebars from 'handlebars';
// Date formatting helper
Handlebars.registerHelper('formatDate', function(date) {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
});
// Currency formatting helper
Handlebars.registerHelper('currency', function(amount) {
return `$${amount.toFixed(2)}`;
});
// Conditional comparison helper
Handlebars.registerHelper('ifEquals', function(arg1, arg2, options) {
return (arg1 === arg2) ? options.fn(this) : options.inverse(this);
});
// Use in templates:
// {{formatDate orderDate}}
// {{currency totalAmount}}
// {{#ifEquals status "shipped"}}Order shipped!{{/ifEquals}}Using Helpers in Templates
<p>Order Date: {{formatDate orderDate}}</p>
<p>Total: {{currency total}}</p>
{{#ifEquals status "pending"}}
<p>Your order is being processed.</p>
{{/ifEquals}}
{{#ifEquals status "shipped"}}
<p>Your order has been shipped!</p>
{{/ifEquals}}Partials (Reusable Components)
Create reusable template parts with partials:
import Handlebars from 'handlebars';
import { readFile } from 'fs/promises';
// Register a partial
const headerSource = await readFile('./templates/partials/header.hbs', 'utf-8');
Handlebars.registerPartial('header', headerSource);
const footerSource = await readFile('./templates/partials/footer.hbs', 'utf-8');
Handlebars.registerPartial('footer', footerSource);templates/partials/header.hbs:
<div style="background-color: #f8f9fa; padding: 20px; text-align: center;">
<img src="{{logoUrl}}" alt="Logo" style="max-width: 200px;" />
</div>templates/partials/footer.hbs:
<div style="background-color: #f8f9fa; padding: 20px; text-align: center; font-size: 12px; color: #666;">
<p>© {{year}} {{companyName}}. All rights reserved.</p>
<p><a href="{{unsubscribeUrl}}">Unsubscribe</a></p>
</div>Main template:
<!DOCTYPE html>
<html>
<body>
{{> header}}
<div style="padding: 20px;">
<h1>{{title}}</h1>
<p>{{message}}</p>
</div>
{{> footer}}
</body>
</html>Common Email Patterns
Password Reset Email
<!DOCTYPE html>
<html>
<body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
<h1>Reset Your Password</h1>
<p>Hi {{userName}},</p>
<p>We received a request to reset your password. Click the button below to create a new password:</p>
<p>
<a href="{{resetUrl}}" style="background-color: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; display: inline-block;">
Reset Password
</a>
</p>
<p>This link will expire in {{expirationHours}} hours.</p>
<p>If you didn't request a password reset, you can safely ignore this email.</p>
</body>
</html>Invoice Email
<!DOCTYPE html>
<html>
<body>
<h1>Invoice #{{invoiceNumber}}</h1>
<p>Hi {{customerName}},</p>
<table style="width: 100%; border-collapse: collapse;">
<thead>
<tr style="background-color: #f8f9fa;">
<th style="padding: 10px; text-align: left;">Description</th>
<th style="padding: 10px; text-align: right;">Amount</th>
</tr>
</thead>
<tbody>
{{#each lineItems}}
<tr>
<td style="padding: 10px; border-bottom: 1px solid #ddd;">{{this.description}}</td>
<td style="padding: 10px; text-align: right; border-bottom: 1px solid #ddd;">${{this.amount}}</td>
</tr>
{{/each}}
</tbody>
<tfoot>
<tr>
<td style="padding: 10px; font-weight: bold;">Total</td>
<td style="padding: 10px; text-align: right; font-weight: bold;">${{total}}</td>
</tr>
</tfoot>
</table>
<p>Due Date: {{dueDate}}</p>
<p><a href="{{paymentUrl}}">Pay Invoice</a></p>
</body>
</html>Notification Email
<!DOCTYPE html>
<html>
<body>
<h2>{{notificationTitle}}</h2>
<p>{{notificationMessage}}</p>
{{#if actionRequired}}
<p style="background-color: #fff3cd; padding: 10px; border-left: 4px solid #ffc107;">
<strong>Action Required:</strong> {{actionMessage}}
</p>
<p>
<a href="{{actionUrl}}" style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">
{{actionButtonText}}
</a>
</p>
{{/if}}
<p style="font-size: 12px; color: #666;">
Sent at {{timestamp}}
</p>
</body>
</html>Handlebars templates are logic-less by design. Keep complex logic in your application code and pass processed data to templates.
Best Practices
Keep Templates Simple: Move complex logic to your application code. Templates should focus on presentation.
Use Partials: Create reusable header, footer, and component partials to maintain consistency.
Register Helpers: Create custom helpers for common formatting operations instead of processing data before template compilation.
Cache Compiled Templates: Compile templates once and reuse them. Don't recompile on every email send.
Escape HTML: Handlebars automatically escapes HTML in {{variable}}. Use {{{variable}}} for raw HTML (only with trusted data).
Test with Real Data: Always test your templates with realistic data including edge cases (long names, special characters, etc.).
TypeScript Support
Define types for your template data:
import Handlebars from 'handlebars';
interface WelcomeEmailData {
userName: string;
dashboardUrl: string;
signupDate: string;
}
async function sendWelcomeEmail(data: WelcomeEmailData) {
const template = Handlebars.compile<WelcomeEmailData>(`
<h1>Welcome {{userName}}!</h1>
<p>Signed up on {{signupDate}}</p>
<a href="{{dashboardUrl}}">Dashboard</a>
`);
const html = template(data);
// Send with Keplars
await fetch('https://api.keplars.com/api/v1/send-email/async', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.KEPLERS_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
to: [data.email],
subject: 'Welcome!',
body: html
})
});
}Resources
Official Documentation: https://handlebarsjs.com
Built-in Helpers: https://handlebarsjs.com/guide/builtin-helpers.html
Nodemailer Integration: https://nodemailer.com
Next Steps
- MJML - Responsive email framework
- React Email - Component-based templates with React
- Send Emails - Learn more about sending emails with Keplars