Sending emails

A useful feature in many contexts.

When a user creates an account, we could send an email to him confirming that his account has been successfully created.

This solution was inspired by this article from Marc Stammerjohann (blog).

First, we should install handlebars. It is a template engine, capable of generating an HTML template dynamically. It allows for holding the email structure while also receiving data.

yarn add hbs

After that, install the dependencies required for email handling.

yarn add @nestjs-modules/mailer nodemailer
yarn add -D @types/nodemailer

Note that, in this case, we use a community package instead of an official one.

We should then create an account with an email provider, so that we can send emails through the system by using its credentials. Here, we'll use MailTrap due to its simple setup. Create a free account and go to the Email Testing section, in order to obtain the credentials. Due to its testing nature, we won't be sending an actual email, but we'll be able to see if it succeeded on the dashboard. That is, if an actual email would be sent in a real case.

Now, create a module and a service for mailing. Also export the service.

nest g mo mail
nest g s mail

In the same directory, create the folder templates, for holding the handlebars templates.

Then, define the environment variables for the email server, Use your actual credentials here, but set this sample data in the example file.

MAIL_PROTOCOL = smtp
MAIL_HOST = smtp.example.com
MAIL_USER = username
MAIL_PASSWORD = pass123
MAIL_FROM = noreply@mail.com

Also update the ENV_VALIDATION_SCHEMA, and break a line for better readability. Note the new validation rules.

MAIL_PROTOCOL: Joi.valid('smtp', 'smtps').required(),
MAIL_HOST: Joi.string().hostname().required(),
MAIL_USER: Joi.required(),
MAIL_PASSWORD: Joi.required(),
MAIL_FROM: Joi.string().email().required(),

As we are used to, the next step is to create a configuration namespace. We may do so in mail/config/mail.config. We use the credentials to create the transport url, very similarly to the database configuration. Also, a default is used for the from email, and the template engine is also configured.

export default registerAs('mail', () => {
  const protocol = process.env.MAIL_PROTOCOL;
  const host = process.env.MAIL_HOST;
  const user = process.env.MAIL_USER;
  const password = process.env.MAIL_PASSWORD;
  const from = process.env.MAIL_FROM;

  const transport = `${protocol}://${user}:${password}@${host}`;

  const config = {
    transport,
    defaults: {
      from: `No Reply <${from}>`,
    },
    template: {
      dir: resolve(__dirname, '..', 'templates'),
      adapter: new HandlebarsAdapter(),
      options: { strict: true },
    },
  } as const satisfies MailerOptions;
  return config;
});

Note the following about the template:

  • The dir location is resolved from __dirname, which is the current folder

  • The HandlebarsAdapter should be imported manually from @nestjs-modules/mailer/dist/adapters/handlebars.adapter

  • The strict option enforces that all fields are passed to the template

In the MailModule, import the MailerModule, passing this configuration.

MailerModule.forRootAsync(mailConfig.asProvider())

Then, inside the templates folder, we may create the template account-confirmation.hbs.

<p>Hello {{name}},</p>

<p>Your account has been successfully created!</p>

<p><strong>The Conrod Shop</strong></p>

However, the templates are not included in the build by default. Due to this, let's go to the nest-cli.json file and add the following inside the compilerOptions. We also enable watch mode for them.

"assets": ["**/*.hbs"],
"watchAssets": true

In the MailService, we may now inject the MailerService. Finally, let's create a method for sending an "account created" confirmation email to the user. We also pass the context with the data for the template. And return the result just in case we may want to inspect it.

sendAccountConfirmation(user: User) {
  const { name, email } = user;

  const mailData = {
    to: email,
    subject: 'Conrod Shop - Account created',
    template: './account-confirmation',
    context: { name },
  } as const satisfies ISendMailOptions;

  return this.mailerService.sendMail(mailData);
}

Now, we should import the MailModule in the UsersModule, and inject the MailService in the UsersSubscriber. It is here that we'll send the email after the account is created, in order to prevent bloating the UsersService. Lastly, to only send this email after the user is successfully created, this will be done in the afterInsert() listener.

async afterInsert(event: InsertEvent<User>) {
  const { entity: user } = event;

  await this.mailService.sendAccountConfirmation(user);
}

To avoid adding a delay to the user creation, we could simply not await the email to be sent. But then, we would not be sure if this operation was successful.

Commit - Sending email when user is created

Last updated