Send Templated Emails in NodeJS

Learn how to use NodeMailer, Handlebars, and juice to send emails effortlessly in NodeJS.

Send Templated Emails in NodeJS
Photo by Stephen Phillips - Hostreviews.co.uk / Unsplash

You built an app and now you need to email users. Things like password reset, account limit reached, or maybe even marketing emails need to be sent.

In this post, I'll show you how to use NodeMailer and Handlebars.js to send emails in a simple and efficient way.

Sending Emails using NodeMailer

NodeMailer has a concept of transports that makes it possible to use it for sending emails from various providers using either SMTP or even proprietary APIs.

In this post, I'll show you how to use the SMTP transport locally but feel free to configure another transport instead.

To test emails, we'll use a tool called MailHog. It provides an SMTP server and a client, as well as a web interface for viewing emails — perfect for local testing.

Create a Docker Compose file to use MailHog:

services:

  mailhog:
    image: mailhog/mailhog
    ports:
      - "127.0.0.1:1025:1025"
      - "127.0.0.1:8025:8025"

compose.yml

The mailhog container will expose ports 1025 and 8025 on your local machine. Port 1025 is used by the SMTP server, while 8025 is for the web interface.

After running the stack using docker-compose up, you can visit http://localhost:8025 in your web browser to see MailHog's web interface.

Now, let's configure NodeMailer to use this SMTP server:

import { createTransport } from "nodemailer";

const transport = createTransport({
  host: "localhost",
  port: 1025,
  secure: false,
  ignoreTLS: true,
});

sendEmail.mjs

After that, create a helper function to send emails in a generic way:

...
async function sendEmail(toAddresses, fromAddress, subject, html, text) {
    const info = await transport.sendMail({
    from: fromAddress,
    to: toAddresses,
    subject,
    text,
    html,
  });
  console.log(`Email sent: ${info.messageId}`);
}

export default sendEmail

sendEmail.mjs

You can test the function like so:

async function sendWelcomeEmail(user) {
  const html = `<h1>Welcome to my app</h1><p>Hello ${user.firstName},</p><p>Nice to have you here!</p>`;
  
  const text = `Hello ${user.firstName}, Nice to have you here!`
  
  await sendEmail("[email protected]", "Me <[email protected]>", "Welcome to my app", html, text);
}

Notice how we're building the email's HTML and text contents in the function itself. Isn't this unwieldy? You bet!

Let's build the contents in a less painful manner instead.

Templating with Handlebars

Instead of using template strings to build your email's contents, use a templating system that was designed just for that.

Here, we'll use Handlebars because it's stable and easy to use.

First, create your templates. I recommend using a responsive template like this one for your HTML.

You should create two files, one for the HTML content, and another for plain text.

I like to put those in a directory called templates in my sources root directory.

Now, write a utility function that will compile and render templates:

import { readFile } from "fs/promises";
import Handlebars from "handlebars";
import path from "path";

async function renderToString(templatePath, templateContext) {
  let fullPath = path.join("your-project-sources-root-path", "templates");
  const templateParts = templatePath.split("/");
  templateParts.forEach(part => {
    fullPath = path.join(fullPath, part);
  });
  const templateContents = await readFile(fullPath, "utf-8");
  const template = Handlebars.compile(templateContents);
  return template(templateContext);
}

This function accepts the path to the template as a string that looks like a-path/my-template.ext

The context is an object containing data that will be used in the template. You'll see how it works next.

For example, the new text template will look like so:

Hello {{ user.firstName }},

Nice to have you here!

Best regards,
Me

index.txt

Your HTML template will contain the HTML markup and styles along with the Handlebars directives to render data from the context you gave it.

Sending the Email

First, create two functions for loading the HTML and text templates.

async function getHtml(context) {
  const renderedHtml = await renderToString(
    "emails/welcome/index.html,
    context,
  );
  return juice(renderedHtml);
}

async function getText(context) {
  const rendered = await renderToString(
    "emails/welcome/index.txt,
    context,
  );
  return rendered;
}

Notice how we're using a tool called juice before returning the rendered HTML. Some email clients have trouble when CSS is kept separate from the HTML markup, so we're going to send all the CSS inlined in the markup to solve this problem. Juice does that easily.

Now, update the sendWelcomeEmail function to use the new functions you created.

async function sendWelcomeEmail(user) {
  const ctx = {user}
  const [html, text] = await Promise.all([getHtml(ctx), getText(ctx)]);
  await sendEmail("[email protected]", "Me <[email protected]>", "Welcome to My App", html, text)
}

Visit MailHog's web interface and you should see the email in both HTML and text format available.

Conclusion

Sending emails in NodeJS is dead simple thanks to NodeMailer, Handlebars, and the juice CSS inliner.

To summarize, all you have to do is:

  1. Create a transport.
  2. Compile and render your templates.
  3. Inline the CSS for HTML templates.
  4. Send the email.
💡
If you enjoyed this post, subscribe to me newsletter so that you can be notified whenever I post more like it.