In this article, you’ll learn how to send development and production-ready emails with Nodemailer, Node.js, PostgreSQL, and TypeORM.

Related Post: Backend

  1. API with Node.js + PostgreSQL + TypeORM: Project Setup
  2. API with Node.js + PostgreSQL + TypeORM: JWT Authentication
  3. API with Node.js + PostgreSQL + TypeORM: Send Emails
  4. Node.js, Express, TypeORM, PostgreSQL: CRUD Rest API
  5. Node.js and PostgreSQL: Upload and Resize Multiple Images
API with Node.js + PostgreSQL + TypeORM Send Emails

Related Articles:

Send Emails with Node.js, PostgreSQL, TypeORM Overview

The user provides his credentials to register for an account

registration form with no validation errors react hook form and zod

The verification code is sent to the user’s email

API with Node.js PostgreSQL TypeORM email verification page

The user opens the verification email and then clicks on the ‘Verify Your Account’ button.

API with Node.js + PostgreSQL + TypeORM send emails with nodemailer

The user is then redirected to the Email verification page and the verification code will be pre-filled.

API with Node.js + PostgreSQL + TypeORM send verification code

Now, when the user clicks on the ‘Verify Email’ button, the server will then verify the code and update the user’s information assuming the verification code is valid.

The frontend will then receive the success message and redirect the user to the login page.

API with Node.js + PostgreSQL + TypeORM email verified

Setting up ExpressJs Templating Engine

A template engine allows us to create reusable components called partials and base files. We can then import the partial files and reuse them in other files.

One interesting feature of templating engines is they allow us to dynamically change variables in any of the files.

There are a lot of templating engines out there and some of the popular ones include Pug, Handlebars, Swig, Mustache, EJS, and so on.

In this article, we’ll use Pug as the templating engine to create the email templates.

Most of the popular templating engines work out-of-the-box with ExpressJs. You only need to tell ExpressJs the template engine you want to use.

Run this command to install the necessary dependencies:


yarn add nodemailer pug html-to-text && yarn add -D @types/nodemailer @types/html-to-text @types/pug

Configure ExpressJs to use Pug as the Templating Engine

Now, let’s use the app.set(name, value) method to configure the behavior of the ExpressJs server.

To configure Express to use Pug as its templating engine under the hood, we use app.set('view engine', 'pug') .

Also, we need to tell ExpressJs the folder in which the Pug files will be located with app.set('views', '${__dirname}/views'); .

src/app.ts


AppDataSource.initialize()
  .then(async () => {
  //  (...)

    const app = express();

    // TEMPLATE ENGINE
    app.set('view engine', 'pug');
    app.set('views', `${__dirname}/views`);

    // (...)
  })
  .catch((error) => console.log(error));

We need to provide Nodemailer with SMTP server credentials. There are many services that provide SMTP capabilities but in this article, we are going to use the SMTP server that comes with Nodemailer.

Run these code snippets above the App initializer to generate the SMTP server credentials and delete the code after.

src/app.ts


import nodemailer from 'nodemailer';

(async function () {
  const credentials = await nodemailer.createTestAccount();
  console.log(credentials);
})();

// (...)

Assuming your app is running, you should get the credentials in the terminal.

nodemailer test credentials

Copy and update the .env file with the credentials.

.env


EMAIL_USER=ypi5eci55z2an5pm@ethereal.email
EMAIL_PASS=5B2N9Pn1ETvmXxDCye
EMAIL_HOST=smtp.ethereal.email
EMAIL_PORT=587

Also, update the custom-environment-variables.ts file with the SMTP credentials you provided in the .env file.

config/custom-environment-variables.ts


export default {
  port: 'PORT',
  postgresConfig: {
    host: 'POSTGRES_HOST',
    port: 'POSTGRES_PORT',
    username: 'POSTGRES_USER',
    password: 'POSTGRES_PASSWORD',
    database: 'POSTGRES_DB',
  },

  accessTokenPrivateKey: 'JWT_ACCESS_TOKEN_PRIVATE_KEY',
  accessTokenPublicKey: 'JWT_ACCESS_TOKEN_PUBLIC_KEY',
  refreshTokenPrivateKey: 'JWT_REFRESH_TOKEN_PRIVATE_KEY',
  refreshTokenPublicKey: 'JWT_REFRESH_TOKEN_PUBLIC_KEY',

  smtp: {
    host: 'EMAIL_HOST',
    pass: 'EMAIL_PASS',
    port: 'EMAIL_PORT',
    user: 'EMAIL_USER',
  },
};

Lastly, update the validateEnv.ts to ensure that the variables we provided in the .env have the correct TypeScript types.

src/utils/validateEnv.ts


import { cleanEnv, port, str } from 'envalid';

const validateEnv = () => {
  cleanEnv(process.env, {
    NODE_ENV: str(),
    PORT: port(),

    POSTGRES_HOST: str(),
    POSTGRES_PORT: port(),
    POSTGRES_USER: str(),
    POSTGRES_PASSWORD: str(),
    POSTGRES_DB: str(),

    JWT_ACCESS_TOKEN_PRIVATE_KEY: str(),
    JWT_ACCESS_TOKEN_PUBLIC_KEY: str(),
    JWT_REFRESH_TOKEN_PRIVATE_KEY: str(),
    JWT_REFRESH_TOKEN_PUBLIC_KEY: str(),

    EMAIL_USER: str(),
    EMAIL_PASS: str(),
    EMAIL_HOST: str(),
    EMAIL_PORT: port(),
  });
};

export default validateEnv;


Complete App.ts code

Below is what your app.ts file should look like after configuring the App to use Pug as the templating engine.

src/app.ts


require('dotenv').config();
import express, { NextFunction, Request, Response } from 'express';
import config from 'config';
import morgan from 'morgan';
import cookieParser from 'cookie-parser';
import cors from 'cors';
import { AppDataSource } from './utils/data-source';
import AppError from './utils/appError';
import authRouter from './routes/auth.routes';
import userRouter from './routes/user.routes';
import validateEnv from './utils/validateEnv';
import redisClient from './utils/connectRedis';

AppDataSource.initialize()
  .then(async () => {
    // VALIDATE ENV
    validateEnv();

    const app = express();

    // TEMPLATE ENGINE
    app.set('view engine', 'pug');
    app.set('views', `${__dirname}/views`);

    // MIDDLEWARE

    // 1. Body parser
    app.use(express.json({ limit: '10kb' }));

    // 2. Logger
    if (process.env.NODE_ENV === 'development') app.use(morgan('dev'));

    // 3. Cookie Parser
    app.use(cookieParser());

    // 4. Cors
    app.use(
      cors({
        origin: config.get<string>('origin'),
        credentials: true,
      })
    );

    // ROUTES
    app.use('/api/auth', authRouter);
    app.use('/api/users', userRouter);

    // HEALTH CHECKER
    app.get('/api/healthChecker', async (_, res: Response) => {
      const message = await redisClient.get('try');

      res.status(200).json({
        status: 'success',
        message,
      });
    });

    // UNHANDLED ROUTE
    app.all('*', (req: Request, res: Response, next: NextFunction) => {
      next(new AppError(404, `Route ${req.originalUrl} not found`));
    });

    // GLOBAL ERROR HANDLER
    app.use(
      (error: AppError, req: Request, res: Response, next: NextFunction) => {
        error.status = error.status || 'error';
        error.statusCode = error.statusCode || 500;

        res.status(error.statusCode).json({
          status: error.status,
          message: error.message,
        });
      }
    );

    const port = config.get<number>('port');
    app.listen(port);

    console.log(`Server started on port: ${port}`);
  })
  .catch((error) => console.log(error));


Create a Utility Class to Send Emails

Here comes the actual logic for sending any kind of email. I decided to use a Class where we can chain methods to send different kinds of emails.

Require the Nodemailer Credentials

Let’s get the Nodemailer SMTP credentials with the config package.

src/utils/email.ts


const smtp = config.get<{
  host: string;
  port: number;
  user: string;
  pass: string;
}>('smtp');

Define the Email Class Attributes

Next, let’s define the class and the necessary attributes we need for the Mail Options.

src/utils/email.ts


// ? SMTP configurations

export default class Email {
  firstName: string;
  to: string;
  from: string;
  constructor(public user: User, public url: string) {
    this.firstName = user.name.split(' ')[0];
    this.to = user.email;
    this.from = `Codevo ${config.get<string>('emailFrom')}`;
  }

  // (...)
}


Create a Nodemailer Transporter

Next, define a private method in the Class to return the Nodemailer transport. The nodemailer.createTransport({}) method accepts an argument object containing the SMTP configuration options.

src/utils/email.ts


// ? SMTP configurations

export default class Email {
  // ? Constructor

  private newTransport() {
    // if (process.env.NODE_ENV === 'production') {
    //   console.log('Hello')
    // }

    return nodemailer.createTransport({
      ...smtp,
      auth: {
        user: smtp.user,
        pass: smtp.pass,
      },
    });
  }
}

Create a Method to Generate the Email Templates

Now, with the help of pug.renderFile() method, let’s programmatically access any of the Pug files in the views directory and populate them with the email options.

Also, let’s provide the sendMail() method on the transport object with the mail option object.

src/utils/email.ts


// ? SMTP configurations

export default class Email {
// ? Constructor

// ? Transport

  private async send(template: string, subject: string) {
    // Generate HTML template based on the template string
    const html = pug.renderFile(`${__dirname}/../views/${template}.pug`, {
      firstName: this.firstName,
      subject,
      url: this.url,
    });
    // Create mailOptions
    const mailOptions = {
      from: this.from,
      to: this.to,
      subject,
      text: convert(html),
      html,
    };

    // Send email
    const info = await this.newTransport().sendMail(mailOptions);
    console.log(nodemailer.getTestMessageUrl(info));
  }
}

Create a Method to Send the Emails

Next, let’s define the public methods that will call the send('name of template', 'email subject') with the template name without the .pug extension and the email subject.

Here I defined two methods to send the email verification code and a password reset code.

src/utils/email.ts


/ ? SMTP configurations

export default class Email {
  // ? Constructor

  // ? Transport

  // ? Method to send emails

  async sendVerificationCode() {
    await this.send('verificationCode', 'Your account verification code');
  }

  async sendPasswordResetToken() {
    await this.send(
      'resetPassword',
      'Your password reset token (valid for only 10 minutes)'
    );
  }
}

Complete Send Email Utility Class Code

src/utils/email.ts


import nodemailer from 'nodemailer';
import { User } from '../entities/user.entity';
import config from 'config';
import pug from 'pug';
import { convert } from 'html-to-text';

const smtp = config.get<{
  host: string;
  port: number;
  user: string;
  pass: string;
}>('smtp');

export default class Email {
  firstName: string;
  to: string;
  from: string;
  constructor(public user: User, public url: string) {
    this.firstName = user.name.split(' ')[0];
    this.to = user.email;
    this.from = `Codevo ${config.get<string>('emailFrom')}`;
  }

  private newTransport() {
    // if (process.env.NODE_ENV === 'production') {
    //   console.log('Hello')
    // }

    return nodemailer.createTransport({
      ...smtp,
      auth: {
        user: smtp.user,
        pass: smtp.pass,
      },
    });
  }

  private async send(template: string, subject: string) {
    // Generate HTML template based on the template string
    const html = pug.renderFile(`${__dirname}/../views/${template}.pug`, {
      firstName: this.firstName,
      subject,
      url: this.url,
    });
    // Create mailOptions
    const mailOptions = {
      from: this.from,
      to: this.to,
      subject,
      text: convert(html),
      html,
    };

    // Send email
    const info = await this.newTransport().sendMail(mailOptions);
    console.log(nodemailer.getTestMessageUrl(info));
  }

  async sendVerificationCode() {
    await this.send('verificationCode', 'Your account verification code');
  }

  async sendPasswordResetToken() {
    await this.send(
      'resetPassword',
      'Your password reset token (valid for only 10 minutes)'
    );
  }
}


Creating the Email Templates with Pug

Click here to download the email template files. After the download is complete extract the views folder then move it to the src folder of the project.

Updating the User Entity

Next, let’s define a static method on the user entity to generate the verification code and return the hashed and unhashed code from the method.

src/entities/user.entity.ts


import crypto from 'crypto';
import { Entity } from 'typeorm';
import Model from './model.entity';

@Entity('users')
export class User extends Model {
//  (...)

  static createVerificationCode() {
    const verificationCode = crypto.randomBytes(32).toString('hex');

    const hashedVerificationCode = crypto
      .createHash('sha256')
      .update(verificationCode)
      .digest('hex');

    return { verificationCode, hashedVerificationCode };
  }
}


Complete User Entity Code

src/entities/user.entity.ts


import crypto from 'crypto';
import { Entity, Column, Index, BeforeInsert } from 'typeorm';
import bcrypt from 'bcryptjs';
import Model from './model.entity';

export enum RoleEnumType {
  USER = 'user',
  ADMIN = 'admin',
}

@Entity('users')
export class User extends Model {
  @Column()
  name: string;

  @Index('email_index')
  @Column({
    unique: true,
  })
  email: string;

  @Column()
  password: string;

  @Column({
    type: 'enum',
    enum: RoleEnumType,
    default: RoleEnumType.USER,
  })
  role: RoleEnumType.USER;

  @Column({
    default: 'default.png',
  })
  photo: string;

  @Column({
    default: false,
  })
  verified: boolean;

  @Index('verificationCode_index')
  @Column({
    type: 'text',
    nullable: true,
  })
  verificationCode!: string | null;

  @BeforeInsert()
  async hashPassword() {
    this.password = await bcrypt.hash(this.password, 12);
  }

  static async comparePasswords(
    candidatePassword: string,
    hashedPassword: string
  ) {
    return await bcrypt.compare(candidatePassword, hashedPassword);
  }

  static createVerificationCode() {
    const verificationCode = crypto.randomBytes(32).toString('hex');

    const hashedVerificationCode = crypto
      .createHash('sha256')
      .update(verificationCode)
      .digest('hex');

    return { verificationCode, hashedVerificationCode };
  }
}


Running TypeORM Migration to Update PostgreSQL

Next, let’s run the TypeORM migration to update the PostgreSQL database.

package.json


{
 "scripts": {
    "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
    "build": "tsc -p .",
    "typeorm": "typeorm-ts-node-commonjs",
    "migrate": "rm -rf build && yarn build && yarn typeorm migration:generate ./src/migrations/added-entity -d ./src/utils/data-source.ts",
    "db:push": "rm -rf build && yarn build && yarn typeorm migration:run -d src/utils/data-source.ts"
  }
}

Run this command to create a new migration and push the changes to the PostgreSQL database.


yarn migrate && yarn db:push

Update the User Register Controller

Now, let’s update the user registration logic to send the verification email after the user has been created and added to the database.

src/controllers/auth.controller.ts


import Email from '../utils/email';

export const registerUserHandler = async (
  req: Request<{}, {}, CreateUserInput>,
  res: Response,
  next: NextFunction
) => {
  try {
    const { name, password, email } = req.body;

    const newUser = await createUser({
      name,
      email: email.toLowerCase(),
      password,
    });

    const { hashedVerificationCode, verificationCode } =
      User.createVerificationCode();
    newUser.verificationCode = hashedVerificationCode;
    await newUser.save();

    // Send Verification Email
    const redirectUrl = `${config.get<string>(
      'origin'
    )}/verifyemail/${verificationCode}`;

    try {
      await new Email(newUser, redirectUrl).sendVerificationCode();

      res.status(201).json({
        status: 'success',
        message:
          'An email with a verification code has been sent to your email',
      });
    } catch (error) {
      newUser.verificationCode = null;
      await newUser.save();

      return res.status(500).json({
        status: 'error',
        message: 'There was an error sending email, please try again',
      });
    }
  } catch (err: any) {
    if (err.code === '23505') {
      return res.status(409).json({
        status: 'fail',
        message: 'User with that email already exist',
      });
    }
    next(err);
  }
};

Here is a breakdown of what I did above:

  • First, I registered the user
  • Then I called the User.createVerificationCode() static method to generate the hash and unhashed verification code.
  • Next, I sent the unhashed code to the user’s email and stored the hashed code in the database.
  • Lastly, I set the verificationCode column in the database to null when an error occurs during the sending of the email.

Add a Controller to Verify the Email

Next, let’s define the schema for the params that will come from the Email verification route. This step is only needed for TypeScript IntelliSense.

src/schemas/user.schema.ts


export const verifyEmailSchema = object({
  params: object({
    verificationCode: string(),
  }),
});

export type VerifyEmailInput = TypeOf<typeof verifyEmailSchema>['params'];


To verify the verification code we sent to the user’s email, we need to define a controller that will be called when the user makes a GET request to /api/auth/verifyemail/verificationCode route.

src/controllers/auth.controller.ts


import crypto from 'crypto';

export const verifyEmailHandler = async (
  req: Request<VerifyEmailInput>,
  res: Response,
  next: NextFunction
) => {
  try {
    const verificationCode = crypto
      .createHash('sha256')
      .update(req.params.verificationCode)
      .digest('hex');

    const user = await findUser({ verificationCode });

    if (!user) {
      return next(new AppError(401, 'Could not verify email'));
    }

    user.verified = true;
    user.verificationCode = null;
    await user.save();

    res.status(200).json({
      status: 'success',
      message: 'Email verified successfully',
    });
  } catch (err: any) {
    next(err);
  }
};

Here is what I did above:

  • First, I created a hashed form of the verification code sent by the user.
  • Since we already have the hashed verification code in the database, I then made a query to find the user with that hashed verification code.
  • If the user doesn’t exist in the database then it means the verification code provided by the user is invalid.
  • I then updated the verified = true and verificationCode = null if the user exists in the database.

Next, add the email verification code route handler to the routes files.

src/routes/auth.routes.ts


// Verify Email Address
router.get(
  '/verifyemail/:verificationCode',
  validate(verifyEmailSchema),
  verifyEmailHandler
);

Update the Login Controller

Also, update the login controller logic to check if the user is verified before proceeding to check if the password is correct.


export const loginUserHandler = async (
  req: Request<{}, {}, LoginUserInput>,
  res: Response,
  next: NextFunction
) => {
  try {
    const { email, password } = req.body;
    const user = await findUserByEmail({ email });

    // 1. Check if user exist
    if (!user) {
      return next(new AppError(400, 'Invalid email or password'));
    }

    // 2. Check if the user is verified
    if (!user.verified) {
      return next(new AppError(400, 'You are not verified'));
    }

    //3. Check if password is valid
    if (!(await User.comparePasswords(password, user.password))) {
      return next(new AppError(400, 'Invalid email or password'));
    }

    // 4. Sign Access and Refresh Tokens
    const { access_token, refresh_token } = await signTokens(user);

    // 5. Add Cookies
    res.cookie('access_token', access_token, accessTokenCookieOptions);
    res.cookie('refresh_token', refresh_token, refreshTokenCookieOptions);
    res.cookie('logged_in', true, {
      ...accessTokenCookieOptions,
      httpOnly: false,
    });

    // 6. Send response
    res.status(200).json({
      status: 'success',
      access_token,
    });
  } catch (err: any) {
    next(err);
  }
};

Conclusion

In this article, you learned how to send any kind of email with Node.js, TypeScript, Nodemailer, TypeORM, PostgreSQL, and Docker.

Check out the source code on GitHub