Keplars

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 handlebars

For email-specific features with Nodemailer:

npm install nodemailer nodemailer-express-handlebars handlebars

Basic 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>&copy; {{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

On this page