Send emails with Handlebars, nodemailer and Gmail

August 28, 2022

Mail

In a recent post we created an email backend using Node and nodemailer, and now we’ll move on to improve that same backend with Handlebars templates. You can check out that post here, and use the same final code.

To make our lives a bit easier without having to create a specific compiler, we’ll use the ready made one nodemailer-express-handlebars

Prequisites

  • Code used in earlier post
  • npm install nodemailer-express-handlebars

What is Handlebars.js?

Handlebars.js extends the Mustache templating language. According to the documentation, Mustache templates are logicless, meaning that they contain no if-else clauses or for loops. They simply expand the tags in the template and adds the values that we have provided1.

This is where Handlebars.js comes in. Handlebars provides us with all the good things from Mustache templates, but it also gives us the ability to handle logic.

Creating a basic Handlebars template

Let’s start by creating a new directory called templates, and inside that create email_template.hbs. This means we’ve got the following folder setup in our backend:

  • index.js
  • templates
    • email_template.hbs

For now, we’re just going to move the code from our earlier example to this, so the email_template.hbs will look like this:

<div>Welcome {{ name }}! This is my first nodemail!</div>

The only change we’ve made, is ${name} to {{ name }} for handlebars.js to be able to interpret the context. This is how values are defined in the template.

Integrating nodemailer-express-handlebars into our app

To actually be able to use this, we need to modify our nodemailer setup to use our plugin as the compiler. First we need to import the package at the top of our file:

const hbs = require('nodemailer-express-handlebars')

and then we need to actually use the compiler in our transporter:

transporter.use('compile', hbs({
    viewEngine: {
        extname: '.hbs',
        layoutsDir: 'templates/',
        defaultLayout: false,
        partialsDir: 'templates/',
    },
    viewPath: 'templates/',
    extName: '.hbs'
}));

The creators of nodemailer have made it possible to inject plugins, and that’s what we’re doing here. The first thing we tell the transporter is to hook in the plugin at ‘compile’ time. This is the step where the email data is supposedly set, but modifications can still be made, such as modifying the html that we’re doing with handlebars2.

Brief explanation of options

  • viewEngine:
    • extname: The file ending that we use for our handlebars files, can be .handlebars
    • layoutsDir: The relative directory where we keep our templates
    • defaultLayout: We’ll set this to false because apparently it only sends this otherwise
    • partialsDir: Where we store headers, footers and other layouts that are reused
  • viewPath: Pretty much the same as layoutsDir
  • extName: Same as extname

Adding variables to a Handlebars template

Now it’s time to modify the ´send-email´ to accommodate the template. There’s only a few

app.post('/send-email', async (req, res) => {
    const name = 'Anonymous'
    try {
        transporter.sendMail({
            from: 'Anonymous Coder<YOUR_EMAIL>',
            to: 'YOUR_EMAIL',
            subject: 'Your first nodemail',
            template: 'email_template',
            context: {
                name: 'Anonymous Coder'
            }
        }, () => {
            res.status(200).send('Email sent')
        })
    } catch {
        return res.status(400).send('Email not sent')
    }
})

That’s as that! Just remove the html entry form the object, and add template: 'email_template' and the context object.

Arrays in Handlebars

One of the benefits of using Handlebars templates together with nodemailer, is that you can add arrays in the context.

In this example I’ll add an array of objects, for example an order template. We’ll have objects of products, for example like this:

const products = [{
    productId: 1,
    productName: 'Cool Bike',
    price: 1050
},
{
    productId: 2,
    productName: 'Great Lock',
    price: 35
}]

Now we have a simple array of objects, let’s create a new template order_confirmation_template.hbs, and add a new route send-confirmation-email to divide these to templates. Let’s start by adding the new route with the products.

app.post('/send-confirmation-email', async (req, res) => {
    const products = [{
        productId: 1, productName: 'Cool Bike', price: 1050
    }, {
        productId: 2, productName: 'Great Lock', price: 35
    }]
    try {
        transporter.sendMail({
            from: 'Anonymous Coder<YOUR_GMAIL>',
            to: 'YOUR_GMAIL',
            subject: 'Product Confirmation',
            template: 'product_confirmation_template',
            context: {
                name: 'Anonymous Coder', products: products,
            }
        }, () => {
            res.status(200).send('Email sent')
        })
    } catch {
        return res.status(400).send('Email not sent')
    }
})

There’s nothing much new here from the earlier example, we’ve just added the products to the context.

Now on to the Handlebars template.

<div>
    <h1>Order confirmation</h1>
    <div>
        <h2>Thank you {{ name }} for your order! Here is the confirmation order.</h2>
    </div>
    <div style="margin-top: 10px;">
        {{#each products}}
            {{#with this}}
                <h3>Product Name: {{this.productName}}</h3>
                <h4>Price: {{this.price}}</h4>
                <h4>Product ID: {{this.productId}}</h4>
            {{/with}}
        {{/each}}
    </div>
</div>

So to specify an array in a Handlebars template, we simply use #each products or #each <YOUR_ARRAY_IN_CONTEXT>. If this was an array without any objects, we could just use:

{{#each products}}
    <h3>{{this}}</h3>
{{/each}}

to show the user the elements in the array.

The with-helper tells the Handlebar to evaluate the context differently, so for example with the first object this becomes:

const this = {
    productId: 1,
    productName: 'Cool Bike',
    price: 1050
}

and thereafter we can use it as a JS object to get the values of each key.

Final code

In the end, your code should look something like this:

const http = require('http');
const express = require("express");
const nodemailer = require('nodemailer')
const hbs = require('nodemailer-express-handlebars')
const app = express();

const transporter = nodemailer.createTransport({
    service: "Gmail",  
    port: 587, secure: true, auth: {
        user: "YOUR_GMAIL", pass: "YOUR_APPLICATION_PASSWORD"
    },
});

transporter.verify((error, success) => {
    if (error) console.error(error);
    console.log("Server is ready to take our messages");
});

transporter.use('compile', hbs({
    viewEngine: {
        extname: '.hbs',
        layoutsDir: 'templates/',
        defaultLayout: false,
        partialsDir: 'templates/',
    }, viewPath: 'templates/', extName: '.hbs'
}));

app.post('/send-confirmation-email', async (req, res) => {
    const products = [{
        productId: 1, productName: 'Cool Bike', price: 1050
    }, {
        productId: 2, productName: 'Great Lock', price: 35
    }]
    try {
        transporter.sendMail({
            from: 'Anonymous Coder<YOUR_GMAIL>',
            to: 'YOUR_GMAIL',
            subject: 'Product Confirmation',
            template: 'product_confirmation_template',
            context: {
                name: 'Anonymous Coder', products: products,
            }
        }, () => {
            res.status(200).send('Email sent')
        })
    } catch {
        return res.status(400).send('Email not sent')
    }
})

app.post('/send-email', async (req, res) => {
    try {
        transporter.sendMail({
            from: 'Anonymous Coder<YOUR_GMAIL>',
            to: '<YOUR_GMAIL>',
            subject: 'Your first nodemail',
            template: 'email_template',
            context: {
                name: 'Anonymous Coder'
            }
        }, () => {
            res.status(200).send('Email sent')
        })
    } catch {
        return res.status(400).send('Email not sent')
    }
})


const server = http.createServer(app);

server.listen(process.env.PORT || 4000, () => {
    console.log(`Server running on port ${4000}`);
});

Wrap-up

In this post, we’ve gone through how you can use Handlebars.js to power-up your emails.


Profile picture

Written by An anonymous coder who lives and works building useful things. Mostly focusing on Django, Vue and overall Python examples.