In this article, you’ll learn how to send HTML Emails with Node.js, Nodemailer, Prisma, PostgreSQL, Express, and Docker-compose. We’ll create a generic class that can be used to send single or multiple emails at once by chaining the methods together.

CRUD API with Node.js and PostgreSQL Series:

  1. API Node.js, TypeScript, Prisma, PostgreSQL: Project Setup
  2. Node.js + Prisma + PostgreSQL: Access & Refresh Tokens
  3. CRUD API with Node.js and PostgreSQL: Send HTML Emails
  4. API with Node.js, Prisma & PostgreSQL: Forget/Reset Password

Related Articles:

CRUD API with Node.js and PostgreSQL Send HTML Emails

Send Emails with Node.js, Nodemailer, Prisma Overview

Crud Api With Node.Js And Postgresq...
Crud Api With Node.Js And Postgresql Send Html Emails

A user enters his credentials to register for an account

registration form with no validation errors react hook form and zod

The Node.js server validates the credentials before adding the user to the database and sends a verification code to the user’s email.

API with Node.js PostgreSQL TypeORM email verification page

The user clicks on the ‘Verify Your Account’ button upon receiving the verification email.

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

The user is then taken to the email verification page and the verification code is pre-filled in the text input field.

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

When the user clicks on the ‘Verify Email’ button, a GET request is made with the verification code to the Node.js server.

The server then updates the user’s credentials in the database assuming the verification code is valid and sends back a success message.

The frontend application receives the success message and redirects the user to the login page.

API with Node.js + PostgreSQL + TypeORM email verified

Setting up ExpressJs Templating Engine with Pug

A template engine helps us create reusable components called partials which we can import into other files.

With template engines, we define a base template that we can extend in other templates to avoid repeating certain code snippets that appear in most of the template files.

There are numerous templating engines on the internet and some of the popular ones include Pug, Handlebars, Swig, Mustache, EJS, etc.

With all these options at hand, I decided to use Pug to generate all the email templates because I find the syntax clean and simple.

By default, ExpressJs is already configured to work “out-of-the-box” with most of the template engines.

Run the following command to install the required dependencies:


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

Configure ExpressJs to use Pug Templating Engine

The app.set(name, value) method is used to change the behavior of the ExpressJs server.

To configure ExpressJs to use the Pug templating engine, we use app.set('view engine', 'pug') .

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

src/app.ts


const prisma = new PrismaClient();
const app = express();

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

  // ? Remaining code
}

bootstrap()
  .catch((err) => {
    throw err;
  })
  .finally(async () => {
    await prisma.$disconnect();
  });

There are numerous SMTP providers that we can use in conjunction with Nodemailer to send real emails but I decided to use the SMTP server that comes with Nodemailer to avoid sending development emails to real email addresses.

Execute the following code snippets above the bootstrap function to generate the SMTP credentials and remove the code afterward.

src/app.ts


import nodemailer from 'nodemailer';

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

// (...)

Assuming the server is running, you should get the SMTP credentials in the terminal.

nodemailer test credentials

Update the .env file with the SMTP credentials.

.env


EMAIL_USER=maycgho4gm6paqnq@ethereal.email
EMAIL_PASS=RbVrQdN94Ax5CJdnwp
EMAIL_HOST=smtp.ethereal.email
EMAIL_PORT=587

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

config/custom-environment-variables.ts


export default {
  port: 'PORT',

  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',
  },
};

Finally, update the validateEnv.ts file to ensure that the environment variables you provided in the .env have their correct Typescript types.

src/utils/validateEnv.ts


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

function validateEnv() {
  cleanEnv(process.env, {
    DATABASE_URL: str(),

    PORT: port(),
    NODE_ENV: str(),

    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

src/app.ts


require('dotenv').config();
import express, { NextFunction, Request, Response, response } from 'express';
import config from 'config';
import cors from 'cors';
import morgan from 'morgan';
import cookieParser from 'cookie-parser';
import validateEnv from './utils/validateEnv';
import { PrismaClient } from '@prisma/client';
import authRouter from './routes/auth.routes';
import userRouter from './routes/user.routes';
import AppError from './utils/appError';

validateEnv();

const prisma = new PrismaClient();
const app = express();

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

  // MIDDLEWARE

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

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

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

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

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

  // Testing
  app.get('/api/healthchecker', (_, res: Response) => {
    res.status(200).json({
      status: 'success',
      message: 'Welcome to NodeJs with Prisma and PostgreSQL',
    });
  });

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

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

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

  const port = config.get<number>('port');
  app.listen(port, () => {
    console.log(`Server on port: ${port}`);
  });
}

bootstrap()
  .catch((err) => {
    throw err;
  })
  .finally(async () => {
    await prisma.$disconnect();
  });


Create a Utility Class to the Send Emails

The utility class will allow us to send single or multiple emails at once by chaining the different methods together.

Get the Nodemailer Credentials

src/utils/email.ts


import nodemailer from 'nodemailer';
import config from 'config';
import pug from 'pug';
import { convert } from 'html-to-text';
import { Prisma } from '@prisma/client';

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 attributes needed for the Mail Options and the email templates.

src/utils/email.ts


// ? SMTP configurations

export default class Email {
 #firstName: string;
  #to: string;
  #from: string;
  constructor(private user: Prisma.UserCreateInput, private url: string) {
    this.#firstName = user.name.split(' ')[0];
    this.#to = user.email;
    this.#from = `Codevo <admin@admin.com>`;
  }

  // (...)
}


Create a Nodemailer Transporter

Next, define a private method to create and return the Nodemailer transport object. 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 retrieve any of the Pug files in the views folder and render it into an HTML file.

Next, evoke the sendMail() method and provide it with the defined mail options.

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, create public methods to evoke the send('name of template', 'email subject') method with the template name without the .pug extension and the email subject.

Create 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 config from 'config';
import pug from 'pug';
import { convert } from 'html-to-text';
import { Prisma } from '@prisma/client';

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

export default class Email {
  #firstName: string;
  #to: string;
  #from: string;
  constructor(private user: Prisma.UserCreateInput, private url: string) {
    this.#firstName = user.name.split(' ')[0];
    this.#to = user.email;
    this.#from = `Codevo <admin@admin.com>`;
  }

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

    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 Pug HTML email templates. Extract the views folder from the downloaded Zip file and move it to the src folder of the project.

Run Prisma Migration to Update PostgreSQL

Next, generate the Prisma migration file and push the changes to the PostgreSQL database.

package.json


{
  "scripts": {
    "start": "ts-node-dev --respawn --transpile-only --exit-child src/app.ts",
    "migrate": "npx prisma migrate dev --name user-model && npx prisma generate",
    "push": "npx prisma db push",
    "build": "tsc . -p"
  }
}

Run this command to start the PostgreSQL and Redis Docker containers


docker-compose up -d

Next, run the following command to create a new migration and push the changes to the PostgreSQL database.


yarn migrate && yarn push

Update the User Registration Controller

Now, let’s update the user registration handler to send the verification code after the user has been added to the database.

src/controllers/auth.controller.ts


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

export const registerUserHandler = async (
  req: Request<{}, {}, RegisterUserInput>,
  res: Response,
  next: NextFunction
) => {
  try {
    const hashedPassword = await bcrypt.hash(req.body.password, 12);

    const verifyCode = crypto.randomBytes(32).toString('hex');
    const verificationCode = crypto
      .createHash('sha256')
      .update(verifyCode)
      .digest('hex');

    const user = await createUser({
      name: req.body.name,
      email: req.body.email.toLowerCase(),
      password: hashedPassword,
      verificationCode,
    });

    const redirectUrl = `${config.get<string>(
      'origin'
    )}/verifyemail/${verifyCode}`;
    try {
      await new Email(user, redirectUrl).sendVerificationCode();
      await updateUser({ id: user.id }, { verificationCode });

      res.status(201).json({
        status: 'success',
        message:
          'An email with a verification code has been sent to your email',
      });
    } catch (error) {
      await updateUser({ id: user.id }, { verificationCode: null });
      return res.status(500).json({
        status: 'error',
        message: 'There was an error sending email, please try again',
      });
    }
  } catch (err: any) {
    if (err instanceof Prisma.PrismaClientKnownRequestError) {
      if (err.code === 'P2002') {
        return res.status(409).json({
          status: 'fail',
          message: 'Email already exist, please use another email address',
        });
      }
    }
    next(err);
  }
};

Here is a breakdown of what I did above:

  • I first registered the new user
  • Then I used the Node.js crypto module to generate the verification code.
  • Next, I sent the unhashed verification code to the user’s email and stored the hashed one in the database.
  • Lastly, I set the verificationCode column in the PostgreSQL database to null when an error occurs in the process of sending the email.

Create a Controller to Verify the Email

Before creating the email verification code handler, let’s create a schema to validate the/api/auth/verifyemail/verificationCode 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 need to define a controller that will validate the code and update the user’s credentials in the PostgreSQL database.

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 updateUser(
      { verificationCode },
      { verified: true, verificationCode: null },
      { email: true }
    );

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

    res.status(200).json({
      status: 'success',
      message: 'Email verified successfully',
    });
  } catch (err: any) {
    if (err.code === 'P2025') {
      return res.status(403).json({
        status: 'fail',
        message: `Verification code is invalid or user doesn't exist`,
      });
    }
    next(err);
  }
};

Here is what I did above:

  • I first converted the code to a hashed one with the crypto module.
  • Then I made a query to find the user belonging to the hashed verification code.

    If the user doesn’t exist in the database then it means the verification code provided is invalid.
  • Lastly, I updated the verified column to true and verificationCode to null if the user exists in the database.

Next, add the email verification controller to the routes.

src/routes/auth.routes.ts


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

Update the Login Controller

Finally, update the login controller to check if the user is verified before proceeding to check if the password is valid.


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

    const user = await findUniqueUser(
      { email: email.toLowerCase() },
      { id: true, email: true, verified: true, password: true }
    );

    if (!user) {
      return next(new AppError(400, 'Invalid email or password'));
    }

    // Check if user is verified
    if (!user.verified) {
      return next(
        new AppError(
          401,
          'You are not verified, please verify your email to login'
        )
      );
    }

    if (!user || !(await bcrypt.compare(password, user.password))) {
      return next(new AppError(400, 'Invalid email or password'));
    }

    // Sign Tokens
    const { access_token, refresh_token } = await signTokens(user);
    res.cookie('access_token', access_token, accessTokenCookieOptions);
    res.cookie('refresh_token', refresh_token, refreshTokenCookieOptions);
    res.cookie('logged_in', true, {
      ...accessTokenCookieOptions,
      httpOnly: false,
    });

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

Conclusion

Congrats for reaching the end. In this article, you learned how to send HTML emails with Node.js, Nodemailer, Pug, Prisma, Express, PostgreSQL, and Docker-compose.

Check out the source code on GitHub