Setting Up Email in Node.js: A Comprehensive Guide
Written on
Chapter 1: The Importance of Email
Email plays a crucial role in online communication. Its ability to transmit messages electronically dates back before the advent of the internet, making it one of the most vital and universally accepted methods of digital communication. If you're developing online software, integrating email functionality is indispensable.
However, the initial setup can often prove to be more complicated than expected. This article will guide you through the process of incorporating this essential feature into your applications using a reliable tech stack tailored for Node.js. This setup is versatile, suitable for both simple projects and large-scale applications; at my company, we leverage a similar architecture to dispatch system notifications to hundreds of thousands of users daily. For a practical demonstration, refer to this repository.
The Challenges of Sending Emails with Node.js
Working with email in Node.js can introduce several challenges. The Simple Mail Transfer Protocol (SMTP) has limitations compared to HTTP, which is designed for web pages. When it comes to email design, you're restricted to a limited set of HTML and CSS, primarily due to security concerns and the inherent nature of older technology. For instance, if you're trying to create a button, you can't use a standard HTML <button> element; instead, you'll need to resort to other HTML structures.
Compounding these issues are the quirks of legacy email clients, such as Microsoft Outlook. Given its vast user base, Outlook's specific requirements add complexity and reduce code clarity.
Moreover, transitioning from languages like PHP can leave you puzzled, especially since many PHP web servers come with built-in sendmail capabilities, whereas Node.js does not provide this out of the box.
Don't fret! There are various tools available to navigate these challenges. This article will present a straightforward blueprint to seamlessly integrate email functionality into your Node.js applications.
Section 1.1: The Solution
With the appropriate tools, we can tackle the aforementioned challenges effectively. After a bit of initial setup, the rest of the process is relatively smooth.
The primary tools we will utilize include:
- Nodemailer: The leading Node.js library for sending emails.
- MJML: A framework for creating responsive and compatible email designs, along with Handlebars for dynamic content support in MJML templates.
- Amazon SES: A cloud-based SMTP server and interface.
Make sure you have Node.js installed to follow along with this guide. If you prefer video tutorials, check out my video version of this guide below.
Alternative Options
While the tools I've selected are popular and well-established, there are other alternatives that may better suit your specific needs. If Nodemailer doesn't meet your requirements, consider EmailJS. For responsive designs, Foundation for Emails is a good choice. You can explore additional templating systems in my article on HTML templating options. If you're looking for cloud-based SMTP services, consider options like Mailgun and Sendgrid.
Section 1.2: Designing Our Email Service
When creating a new library or service, it's beneficial to envision how the code will be utilized. I want the email-sending process to be simple and intuitive. For example, consider this straightforward implementation:
await email.onboarding.send("[email protected]", variables, options);
In this case, information such as the sender, subject, cc, bcc, attachments, and HTML content can be defined for each template. If there’s a need to override any of these settings, we can do so through the options parameter.
Next, let's craft a basic email service that minimizes the effort required each time a new email template is introduced.
Subsection 1.2.1: Project Setup
We’ll begin by creating a new directory for our application.
mkdir nodejs-nodemailer-tutorial
cd nodejs-nodemailer-tutorial
Once inside, we can initialize a new Node.js project.
npm init -y
Then, we can install the required dependencies.
npm i @aws-sdk/client-ses handlebars mjml nodemailer
I'm opting for TypeScript, which is increasingly becoming the standard for JavaScript server projects. Let's add a basic tsconfig.json file:
{
"compilerOptions": {
"target": "es2016",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true
}
}
To run TypeScript and to include type definitions for some libraries, we’ll add additional development dependencies.
npm i -D @types/mjml @types/nodemailer nodemon ts-node typescript
Finally, modify the package.json to set the main entry point and define a script to run the application.
{
"main": "src/index.ts",
"scripts": {
"start": "ts-node src/index.ts"}
}
The Transporter
First, we need to create a transporter—essentially a client for sending our emails. For production, we’ll utilize Amazon SES, while during development, we can use a mail catcher like Ethereal to preview emails without actually sending them. We’ll switch to the SES version when the NODE_ENV is set to "production".
// src/services/email/transporter.ts
import * as nodemailer from "nodemailer";
import * as aws from "@aws-sdk/client-ses";
function getTransporter() {
if (process.env.NODE_ENV === "production") {
const ses = new aws.SES({
apiVersion: "2010-12-01",
region: "eu-west-2",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID ?? "",
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY ?? "",
},
});
return nodemailer.createTransport({
SES: { ses, aws },});
} else {
return nodemailer.createTransport({
host: "smtp.ethereal.email",
port: 587,
secure: false,
auth: {
user: "[email protected]",
pass: "jn7jnAPss4f63QBp6D",
},
});
}
}
export const transporter = getTransporter();
The Base Email
To share functionality across different email types, we can create a base class that individual email classes can extend. Here's the basic structure.
// src/services/email/base.ts
export abstract class BaseEmail {
protected from: string;
protected subject: string | ((variables: T) => string);
protected template: string;
constructor({
from,
subject,
template,
}: {
from: string;
subject: string | ((variables: T) => string);
template: string;
}) {
this.from = from;
this.subject = subject;
this.template = template;
}
}
We support both static strings and dynamic callbacks for the subject. Now, let's implement a generic send function to manage the email-sending process.
// src/services/email/base.ts
import fs from "fs/promises";
import path from "path";
import mjml2html from "mjml";
import * as nodemailer from "nodemailer";
import * as handlebars from "handlebars";
import { transporter } from "./transporter";
export abstract class BaseEmail {
// existing code...
public async send(
to: string,
variables: T,
options?: nodemailer.SendMailOptions
) {
const mjml = await fs.readFile(this.template, "utf-8");
const html = mjml2html(handlebars.compile(mjml)(variables)).html;
await transporter.sendMail({
to,
from: this.from,
subject: typeof this.subject === "string" ? this.subject : this.subject(variables),
html,
...options,
}).then((info) => {
console.info("Message sent: ", info.messageId);
const previewUrl = nodemailer.getTestMessageUrl(info);
if (previewUrl) {
console.info("Preview URL: ", previewUrl);}
});
}
}
Templates
Now that we have our email structure, it's time to create the templates. Inside the src/email directory, we’ll create a templates folder, along with subfolders for onboarding and referral email templates.
/email/
└── /templates/
├── /onboarding/
│ ├── template.mjml
│ └── index.ts
└── /referral/
├── template.mjml
└── index.ts
Here’s a simple onboarding template:
Hi {{firstName}},
You're invited!
To claim your account and login, click the button below:
Login
In index.ts, we extend the BaseEmail class:
// src/services/email/templates/onboarding/index.ts
import path from "path";
import { BaseEmail } from "../../base";
interface OnboardingEmailVariables {
firstName: string;
loginUrl: string;
}
export class OnboardingEmail extends BaseEmail {
constructor() {
super({
from: "[email protected]",
subject: "Welcome!",
template: path.join(__dirname, "template.mjml"),
});
}
}
For the referral email, we can create a similar structure:
Hello {{firstName}}
You've been referred by {{referrer.firstName}} {{referrer.lastName}}!
Click the button below to claim your account:
Claim Account
And the corresponding index.ts file:
// src/services/email/templates/referral/index.ts
import path from "path";
import { BaseEmail } from "../../base";
interface ReferralEmailVariables {
firstName: string;
referrer: {
firstName: string;
lastName: string;
};
loginUrl: string;
}
export class ReferralEmail extends BaseEmail {
constructor() {
super({
from: "[email protected]",
subject: ({ referrer }: ReferralEmailVariables) => You've been referred by ${referrer.firstName}!,
template: path.join(__dirname, "template.mjml"),
});
}
}
The Email Service
Next, we’ll group our email templates in an EmailService class.
// src/services/email/index.ts
import { OnboardingEmail } from "./templates/onboarding";
import { ReferralEmail } from "./templates/referral";
class EmailService {
onboarding = new OnboardingEmail();
referral = new ReferralEmail();
}
export const email = new EmailService();
Finally, we can test our setup in the root index.ts file:
// src/index.ts
import { email } from "./services/email";
(async () => {
await email.onboarding.send("[email protected]", {
firstName: "John",});
await email.referral.send("[email protected]", {
firstName: "John",
referrer: {
firstName: "Jane",
lastName: "Doe",
},
});
})();
Run the application with:
npm run start
You should see messages like:
Message sent: <[email protected]>
To enhance your emails, you can refer to the first video linked above for a visual guide.
By following this guide, you can set up a robust email-sending system in your Node.js applications. Happy coding!