Post

Implementing Email Subscription and Automatic Email Sending in Next.js with MongoDB-img

Implementing Email Subscription and Automatic Email Sending in Next.js with MongoDB

May 4, 2024

Tansel Berkant Oflaz

Tansel Berkant Oflaz

Next.js Server Actions and a custom MongoDB model to manage subscribers and use Nodemailer to send emails.

Next.js

In this guide, I'll walk you through setting up a simple email subscription feature in a Next.js application using MongoDB as the database. This tutorial will also include sending automatic emails to all subscribers when a new record is added to the database. This setup is perfect for a variety of use cases, such as newsletters or notification systems. We'll utilize Next.js Server Actions and a custom MongoDB model to manage subscribers and use Nodemailer to send emails.

Setting Up the Environment

First, ensure that you have Node.js installed on your machine. Then, create a new Next.js project and add MongoDB and Nodemailer to your dependencies.

npx create-next-app@latest my-email-project
cd my-email-project
npm install mongoose nodemailer

Configuring MongoDB

Before we proceed, ensure you have a MongoDB database set up. You can use a local MongoDB instance or a cloud-based service like MongoDB Atlas. Create a new collection for subscribers, and get your connection string ready.

In your project, create a utility file to handle the MongoDB connection. Here's an example connectDb.ts file:

import mongoose from 'mongoose';

export const connectToDb = async () => {
  if (mongoose.connection.readyState === 1) return;
  const dbUri = process.env.MONGODB_URI || 'your-mongodb-connection-string';
  await mongoose.connect(dbUri);
};

Creating the Subscriber Model

Next, create the Mongoose schema for your subscribers. In a models folder, create Subscriber.ts:

import mongoose from 'mongoose';

const subscriberSchema = new mongoose.Schema(
  {
    email: {
      type: String,
      required: true,
      unique: true,
    },
  },
  { timestamps: true }
);

export const Subscriber = mongoose.models?.Subscriber || mongoose.model('Subscriber', subscriberSchema);

This schema includes a single field for email addresses and timestamps to track when subscribers were added or modified.

Implementing the Email Subscription Form

In your Next.js project, create a new React component for your subscription form. This component will include form submission logic and display messages based on form submission results. Here's an example of a SubscribeForm component:

'use client';

import { useState, useEffect, FormEvent } from 'react';
import { EnvelopeIcon } from '@heroicons/react/20/solid';
import { addSubscriber } from '@/lib/actions';
import { useLocale } from 'next-intl';
import confetti from 'canvas-confetti';

const runConfetti = () => {
  confetti({
    particleCount: 100,
    spread: 70,
    origin: { y: 0.6 },
  });
};

const SubscribeForm = () => {
  const locale = useLocale();
  const [state, formAction] = useFormState(addSubscriber, undefined);
  const [email, setEmail] = useState('');
  const [isExist, setIsExist] = setState(false);
  const [showMessage, setShowMessage] = useState(false);

  useEffect(() => {
    if (state === 'Subscriber saved successfully') {
      setShowMessage(true);
      setIsExist(false);
      setEmail('');
      const timer = setTimeout(() => setShowMessage(false), 3000);
      runConfetti();
      return () => clearTimeout(timer);
    } else if (state === 'Mail allready exist') {
      setShowMessage(true);
      setIsExist(true);
      setEmail('');
      const timer = setTimeout(() => setShowMessage(false), 3000);
      return () => clearTimeout(timer);
    }
  }, [state]);

  const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    formAction(email);
  };

  return (
    <div className="relative mt-2 rounded-md shadow-sm w-[90%] md:w-[80%]">
      <form onSubmit={handleSubmit}>
        <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
          <EnvelopeIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
        </div>
        <input
          type="email"
          name="email"
          className="w-full rounded-md border-0 py-7 pl-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 outline-none sm:text-sm sm:leading-6"
          placeholder="you@example.com"
          onChange={(e) => setEmail(e.target.value)}
          value={email}
        />
        <div className="absolute inset-y-0 right-0 flex items-center">
          <button
            type="submit"
            className="h-full w-full text-white bg-orbitPurple px-2 md:px-4 rounded-r-md"
          >
            Submit
          </button>
        </div>
      </form>
      {showMessage && (
        <p className="absolute text-betOrbitMainSilver mt-4">
          {isExist
            ? locale === 'tr'
              ? 'Zaten bir kaydınız var'
              : 'You have a registration already'
            : locale === 'tr'
            ? 'Kaydınız başarıyla oluşturuldu, teşekkürler...'
            : 'Your registration has been successfully created, thank you...'}
        </p>
      )}
    </div>
  );
};

export default SubscribeForm;

This form captures email addresses and displays appropriate messages when a subscriber is successfully added or when an email already exists.

Handling Subscriber Actions

Now, let's implement the action that will add a new subscriber to the database and return the appropriate message. Create a new file, actions.ts, in your lib folder:

import { connectToDb } from '@/utils/connectDb';
import { Subscriber } from '@/models';

export const addSubscriber = async (prevState: any, email: any) => {
  try {
    await connectToDb();

    // Check if email exists
    const existingSubscriber = await Subscriber.findOne({ email });
    if (existingSubscriber) {
      return 'Mail already exists';
    }

    // Create new subscriber
    const newSubscriber = new Subscriber({ email });
    await newSubscriber.save();
    return 'Subscriber saved successfully';
  } catch (err) {
    console.error('Error adding subscriber:', err);
    return 'An error occurred while registering the subscriber';
  }
};

This action checks whether an email already exists in the database. If not, it adds a new subscriber and returns a success message. Otherwise, it returns a message indicating that the email is already registered.

Sending Emails to All Subscribers

To send emails to all subscribers when a new record is added, we need to set up Nodemailer. Create a new file called sendMail.ts in your lib folder:

import nodemailer from 'nodemailer';
import { readFileSync } from 'fs';
import { join } from 'path';
import { connectToDb } from '@/utils/connectDb';
import { Subscriber } from '@/models';

const sendMail = async () => {
  await connectToDb();

  // Load HTML email template
  const emailTemplatePath = join(process.cwd(), 'emails/email-template.html');
  const emailHtml = readFileSync(emailTemplatePath, 'utf-8');

  const transport = nodemailer.createTransport({
    service: 'gmail',
    auth: {
      user: process.env.NODEMAILER_EMAIL,
      pass: process.env.NODEMAILER_PASSWORD,
    },
  });

  try {
    // Get all subscribers
    const subscribers = await Subscriber.find({});

    if (subscribers.length === 0) {
      console.log('No subscribers found.');
      return;
    }

    // Send an email to each subscriber
    for (const subscriber of subscribers) {
      const mailOptions = {
        from: process.env.NODEMAILER_EMAIL,
        to: subscriber.email,
        subject: 'New Update | BetOrbit',
        html: emailHtml,
      };

      await transport.sendMail(mailOptions);
    }

    console.log('Emails sent successfully to all subscribers.');
  } catch (err) {
    console.error('Failed to send email:', err);
  }
};

export { sendMail };

This file sets up Nodemailer with Gmail and sends an email to each subscriber when invoked. Note that this example uses a pre-defined HTML template for the email content.

Triggering Email Sending

Finally, let's create a function to add new records and trigger email sending. This can be useful for creating new entries and notifying all subscribers. Here's an example addTable.ts file in your lib folder:


import { connectToDb } from '@/utils/connectDb';
import { Tip } from '@/models'; 
import { sendMail } from '@/lib/sendMail';

export const addTable = async (prevState: any, formData: TipsData) => {
  const { day, tips } = formData;

  if (!day || day === '') {
    return 'No date provided';
  }

  try {
    await connectToDb();

    const newCoupon = new Tip({
      day,
      tips,
    });

    await newCoupon.save();

    // Revalidate the page (if necessary)
    revalidatePath('/');

    // Send emails to all subscribers
    await sendMail();

    return 'Table saved successfully';
  } catch (err) {
    console.error('Error adding table:', err);
    return 'Something went wrong';
  }
};

This function adds a new record to the database and triggers the sendMail function to send emails to all subscribers. It also includes error handling for potential issues during the process.

Wrapping Up

With these components in place, you've implemented a basic email subscription system in Next.js with MongoDB and Nodemailer. When new records are added to the database, emails are automatically sent to all subscribers. This setup is flexible and can be used in various applications, from newsletters to event notifications.

Feel free to expand on this guide by adding more sophisticated error handling, security measures, and email templates to suit your specific needs. I hope you found this guide helpful!

© 2024 Tansel Berkant Oflaz. All Rights Reserved.