Set up Nodemailer with Next.js & Typescript

Go back 8 min read

Set up Nodemailer with Next.js, Typescript & React Hook Form

Introduction

Nodemailer is a module for Node.js application to allow easy as cake email sending!

Nodemailer allows services like Gmail, SendGrid, Mailgun, SendinBlue, and more. In this article, I will show you how to set up Nodemailer with Next.js/Typescript.

We are going to use React Hook Form but it is optional.

What are some reasons to use Nodemailer?

  • To subscribe to a newsletter
  • Transactional emails
  • Sending automated messages
  • User verification emails
  • Receive messages from a contact form.

Prerequisites

  • Basic knowledge of Next.js
  • Basic knowledge of Typescript
  • Basic knowledge of React Hook Form

We will begin by starting a fresh new Next.js app with typescript and sign up for SendinBlue. SendinBlue provides 300 free emails daily, but feel free to use any SMTP service of your choice!

When creating a SendinBlue account please find your SMTP KEY PASS. This could differ if you are using a different service. Please refer to the nodemailer documentation.

NOTE: Not sure where to find your SMTP pass after signing up? Click on your name on the top right > SMTP & API, and click on the SMTP tab near the center.

Start your Next.js app with the following command: npm run dev or with yarn or pnpm 😉

Let’s get started! In your .env.local, add the following:

SMTP_PASS='YOUR_SMTP_PASS'

Create a file named contact.ts under pages/api/ and add this small boilerplate below:

// pages/api/contact.ts

import type { NextApiRequest, NextApiResponse } from 'next';
import * as nodemailer from 'nodemailer';

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // code...
}

Let’s add the createTransport method provided by Nodemailer. This allows us to set up our SMTP service.

const transporter = nodemailer.createTransport({
  service: "SendinBlue",
  auth: {
    user: "johndoe@gmail.com", // The account you signed up with SendinBlue
    pass: process.env.SMTP_PASS,
  },
  secure: false,
});

You can see the other possible options to pass in here.

So, what type of mail data do we need? Nodemailer offers a lot of options, but we will only need the following for this article:

  • From
  • To
  • Subject
  • Text
  • Html

Other options available here

And for demonstration purposes, I will send an email to myself.

const { name, email, message } = req.body;

const mailData = {
  from: email,
  to: email,
  subject: `Message from ${name}`,
  text: `${message} | Sent from: ${email}`,
  html: `<div>${message}</div><p>Sent from: ${email}</p>`,
};

As you can see, we are grabbing our data from the req.body

One of the cons (not a big deal) is they don’t provide async/await off the bat so let’s create our own promise.

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // rest of code ...
  // ....

  await new Promise((resolve, reject) => {
    transporter.sendMail(mailData, (err: Error | null, info) => {
      if (err) {
        reject(err);
        return res
          .status(500)
          .json({ error: err.message || "Something went wrong" });
      } else {
        resolve(info.accepted);
        res.status(200).json({ message: "Message sent!" });
      }
    });
  });

  return;
}

To receive the information from SendinBlue dashboard, we use the sendMail method from Nodemailer. As you can see, we are passing our mail data options from earlier. This will let us to know if something went wrong or if everything went through fine.

One thing I left out is handling errors. Here is something we can add to our code above the mailData object

const { name, email, message } = req.body;

if (!message || !name || !message) {
  return res
    .status(400)
    .json({ message: "Please fill out the necessary fields" });
}

You should be able to test the endpoint at http://localhost:3000/api/contact in Postman, Insomnia, or any other tools to check if everything is working fine!

A 200 success on the mail data information

Feel free to exclude information on purpose to ensure errors are working.

Here is the final code for the contact.ts file if it is not working for you!

// pages/api/contact.ts

import type { NextApiRequest, NextApiResponse } from "next";
import * as nodemailer from "nodemailer";

// Nodemailer docs: // https://nodemailer.com/about/
export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // https://nodemailer.com/smtp/
  const transporter = nodemailer.createTransport({
    service: "SendinBlue",
    auth: {
      user: process.env.email,
      pass: process.env.SMTP_PASS,
    },
    secure: false, // Default value but showing for explicitness
  });

  const { name, email, message } = req.body;

  if (!message || !name || !message) {
    return res
      .status(400)
      .json({ message: "Please fill out the necessary fields" });
  }

  // https://nodemailer.com/message/#common-fields
  const mailData = {
    from: email,
    to: email,
    subject: `Message from ${name}`,
    text: `${message} | Sent from: ${email}`,
    html: `<div>${message}</div><p>Sent from: ${email}</p>`,
  };

  await new Promise((resolve, reject) => {
    transporter.sendMail(mailData, (err: Error | null, info) => {
      if (err) {
        reject(err);
        return res
          .status(500)
          .json({ error: err.message || "Something went wrong" });
      } else {
        resolve(info.accepted);
        res.status(200).json({ message: "Message sent!" });
      }
    });
  });

  return;
}

Let’s move over to the client side!

We will not dive deep into UI or CSS much. Please look at the Github link below to see the basic styling I have provided.

Here is a simple boilerplate to begin with to render our form:

// pages/index.tsx

import { useState } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useForm } from 'react-hook-form';

const Home: NextPage = () => {
  return <div>Nodemailer!</div>;
};

export default Home;

Since we are working with TypeScript let’s add our interface to match our form data above our Home function

interface DataProps {
  name: string;
  email: string;
  message: string;
}

As a reminder, we are using React Hook Form. Feel free to use other libraries of your liking!

const {
  register,
  handleSubmit,
  formState: { errors },
} = useForm({
  defaultValues: {
    name: "",
    email: "",
    message: "",
  },
});
  1. The register is to provide us with many options.
  2. The handleSubmit function allows us to submit our form data.
  3. The formState object will allow us to check if there are any errors in our form.

Let’s add the following function to handle our form submission:

const onSubmit = async (data: DataProps) => {
  console.log(data);
};

and then we can set up our form as such:

return (
    <div>
      <form onSubmit={handleSubmit(onSubmit)}>
        <label htmlFor="name">Name</label>
        <input id="name" {...register('name', { required: true })} type="text" />

        <label htmlFor="email">Email</label>
        <input id="email" {...register('email', { required: true })} type="email" />

        <label htmlFor="message">Message</label>
        <input id="message" {...register('message', { required: true })} type="text"/>

        <button type="submit">Submit</button>
      </form>
    </div>

  </div>
)

NOTE: Styling and other important factors are intentionally excluded above.

On the client side, you should now be able to submit the data. Check the console log to make sure everything is going through!

Cool, but let’s get our data sent to SendinBlue with the email provided.

Let’s add the following information to the onSubmit function we created earlier:

const onSubmit = async (data: DataProps) => {
  try {
    const res = await fetch("/api/contact", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(data),
    });

    const body = await res.json();

    if (res.ok) {
      alert(`${body.message} 🚀`);
    }

    if (res.status === 400) {
      alert(`${body.message} 😢`);
    }
  } catch (err) {
    console.log("Something went wrong: ", err);
  }
};

In this function, we are targeting the contact endpoint we created earlier. We are also passing in the data we collected from our form.

Please give it a go! You should now be able to send an email.

WOOHOO 🙌🏽🎉 To see the information on SendinBlue dashboard, click the Transactional tab.

If you made it this far, you should now have a fully functional Nodemailer service!

You can find the full code below, which includes items that were abstracted from you (including the errors!):

import { useState } from 'react';
import type { NextPage } from 'next';
import Head from 'next/head';
import styles from '../styles/Home.module.css';
import { useForm } from 'react-hook-form';

interface DataProps {
  name: string;
  email: string;
  message: string;
}

const Home: NextPage = () => {
  const [isLoading, setIsLoading] = useState(false);

  const {
    register,
    handleSubmit,
    formState: { errors },
  } = useForm({
    defaultValues: {
      name: '',
      email: '',
      message: '',
    },
  });

  const onSubmit = async (data: DataProps) => {
    try {
      setIsLoading(true);
      const res = await fetch('/api/contact', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(data),
      });

      const body = await res.json();

      if (res.ok) {
        alert(`${body.message} 🚀`);
      }

      if (res.status === 400) {
        alert(`${body.message} 😢`);
      }

      setIsLoading(false);
    } catch (err) {
      console.log('Something went wrong: ', err);
    }
  };

  return (
    <div>
      <Head>
        <title>Nodemailer with Next.js</title>
        <meta name="description" content="Generated by create next app" />
        <link rel="icon" href="/favicon.ico" />
      </Head>

      <div className={styles.container}>
        <h3 className={styles.text}>Contact me!</h3>
        <form className={styles.form} onSubmit={handleSubmit(onSubmit)}>
          <label htmlFor="name" className={styles.label}>
            Name
          </label>
          <input
            id="name"
            className={styles.input}
            {...register('name', { required: true })}
            type="text"
          />
          {errors.name && <p className={styles.error}>{errors.name.type}</p>}

          <label htmlFor="email" className={styles.label}>
            Email
          </label>
          <input
            id="email"
            className={styles.input}
            {...register('email', { required: true })}
            type="email"
          />
          {errors.email && <p className={styles.error}>{errors.email.type}</p>}

          <label htmlFor="message" className={styles.label}>
            Message
          </label>
          <input
            id="message"
            className={styles.input}
            {...register('message', { required: true })}
            type="text"
          />
          {errors.message && (
            <p className={styles.error}>{errors.message.type}</p>
          )}

          <button type="submit" disabled={isLoading} className={styles.button}>
            {isLoading ? 'loading...' : 'submit'}
          </button>
        </form>
      </div>
    </div>
  );
};

export default Home;

Full code here: Link

Enjoyed the Blog? Clickhereto buy me Coffee! ☕