Building background email notifications with Next.js, Resend and Trigger.dev
Published: 2024-01-11 09:58:50

## What you will find in this article?

Email notifications are the most common way to keep your users informed about actions taking on your application. Typical notifications include: someone follow you, someone likes your post, someone viewed your content. In this post, we are going to explore how we can create a simple asynchronous email notification system using Next.js, Resend and Trigger.dev.

We will use Next.js as the framework to build our application. We will use Resend to send emails and Trigger.dev to offload and send the emails asynchronously.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jwlb0t41kg2s3djcb072.gif)


## Papermark - the open-source DocSend alternative.

Before we kick it off, let me share Papermark with you. It's an open-source alternative to DocSend that helps you securely share documents and get real-time page-by-page analytics from viewers. It's all open-source!

I would be super happy if you could give us a star! Don't forget to share your thoughts in the comments section ❤️
[https://github.com/mfts/papermark](https://github.com/mfts/papermark)

[![Papermark App](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/igzk8cdssbmla9uf1544.png)](https://github.com/mfts/papermark)

## Setup the project

Let's go ahead and set up our project environment for our email background notification system. We'll be creating a Next.js app, and set up to Resend, and most importantly, Trigger to handle the asynchronous email notifications.

### Setting up Next.js with TypeScript and Tailwindcss

We'll use `create-next-app` to generate a new Next.js project. We'll also be using TypeScript and Tailwind CSS, so make sure to select those options when prompted.

```bash
npx create-next-app

# ---
# you'll be asked the following prompts
What is your project named?  my-app
Would you like to add TypeScript with this project?  Y/N
# select `Y` for typescript
Would you like to use ESLint with this project?  Y/N
# select `Y` for ESLint
Would you like to use Tailwind CSS with this project? Y/N
# select `Y` for Tailwind CSS
Would you like to use the `src/ directory` with this project? Y/N
# select `N` for `src/` directory
What import alias would you like configured? `@/*`
# enter `@/*` for import alias
```

### Install Resend and React-Email

Resend is a developer-first transactional email service. We'll use it to send emails to our users. `react-email` is a React component library that makes it easy to create beautiful emails.

```bash
npm install resend react-email
```

### Install Trigger

Trigger is a background job framework for TypeScript. It allows you to offload long-running tasks from your main application and run them asynchronously. We'll use it to send emails asynchronously.

The Trigger CLI is the easiest way to set up Trigger in your new or existing Next.js project. For more info, check out [their docs](https://trigger.dev/docs/documentation/quickstarts/nextjs).

```bash
npx @trigger.dev/cli@latest init
```

## Building the application

Now that we have our setup in place, we are ready to start building our application. The main features we'll cover are:

- Setup Resend email
- Write a API route to send email
- Add a Trigger job to make the email sending asynchronous

### #1 Setup Resend email

First, we'll need to set up Resend to send emails. We'll create a new file `resend-notification.ts` in our project and add the following code.

```ts
// lib/emails/resend-notification.ts
import { Resend } from "resend";
import { NotificationEmail } from "@/components/emails/notification";

const resend = new Resend(process.env.RESEND_API_KEY!);

export async function sendNotificationEmail({
  name,
  email,
}: {
  name: string | null | undefined;
  email: string | null | undefined;
}) {
  const emailTemplate = NotificationEmail({ name });
  try {
    // Send the email using the Resend API
    await resend.emails.send({
      from: "Marc from Papermark <marc@papermark.io>",
      to: email as string,
      subject: "You have a new view on your document!",
      react: emailTemplate,
    });
  } catch (error) {
    // Log any errors and re-throw the error
    console.log({ error });
    throw error;
  }
}
```

And the notification email template using `react-email` will look like this:

```tsx
// components/emails/notification.tsx
import React from "react";
import {
  Body,
  Button,
  Container,
  Head,
  Heading,
  Html,
  Preview,
  Section,
  Text,
  Tailwind,
} from "@react-email/components";

export default function ViewedDocument({
  name,
}: {
  name: string | null | undefined;
}) {
  return (
    <Html>
      <Head />
      <Preview>See who visited your document</Preview>
      <Tailwind>
        <Body className="bg-white my-auto mx-auto font-sans">
          <Container className="my-10 mx-auto p-5 w-[465px]">
            <Heading className="text-2xl font-normal text-center p-0 mt-4 mb-8 mx-0">
              <span className="font-bold tracking-tighter">Papermark</span>
            </Heading>
            <Heading className="mx-0 my-7 p-0 text-center text-xl font-semibold text-black">
              New Document Visitor
            </Heading>
            <Text className="text-sm leading-6 text-black">
              Your document was just viewed by someone.
            </Text>
            <Text className="text-sm leading-6 text-black">
              You can get the detailed engagement insights like time-spent per
              page and total duration for this document on Papermark.
            </Text>
            <Section className="my-8 text-center">
              <Button
                className="bg-black rounded text-white text-xs font-semibold no-underline text-center"
                href={`${process.env.NEXT_PUBLIC_BASE_URL}/documents`}
                style={{ padding: "12px 20px" }}>
                See my document insights
              </Button>
            </Section>
            <Text className="text-sm">
              Cheers,
              <br />
              The Papermark Team
            </Text>
          </Container>
        </Body>
      </Tailwind>
    </Html>
  );
}
```

### #2 Write a API route to send email

Now, we have our email template ready. We can use it to send emails to our users. We'll create a serverless function that takes the `name` and `email` of the user and sends them an email using the `sendNotificationEmail` function we created earlier.

```ts
// pages/api/send-notification.ts
import { NextApiRequest, NextApiResponse } from "next";
import prisma from "@/lib/prisma";
import { sendViewedDocumentEmail } from "@/lib/emails/resend-notification";

export const config = {
  maxDuration: 60,
};

export default async function handle(
  req: NextApiRequest,
  res: NextApiResponse
) {
  // We only allow POST requests
  if (req.method !== "POST") {
    res.status(405).json({ message: "Method Not Allowed" });
    return;
  }

  // POST /api/send-notification
  try {
    const { viewId } = req.body as {
      viewId: string;
    };

    // Fetch the link to verify the settings
    const view = await prisma.view.findUnique({
      where: {
        id: viewId,
      },
      select: {
        document: {
          select: {
            owner: {
              select: {
                email: true,
                name: true,
              },
            },
          },
        },
      },
    });

    if (!view) {
      res.status(404).json({ message: "View / Document not found." });
      return;
    }

    // send email to document owner that document
    await sendViewedDocumentEmail({
      email: view.document.owner.email as string,
      name: view.document.owner.name as string,
    });

    res.status(200).json({ message: "Successfully sent notification", viewId });
    return;
  } catch (error) {
    console.log("Error:", error);
    return res.status(500).json({ message: (error as Error).message });
  }
}
```

### #3 Add a Trigger job to make the email sending asynchronous

Our email sending function is ready, but we don't want to send emails synchronously and therefore wait until the email is send before the application responds to the user. We want to offload the email sending to a background job. We'll use Trigger to do that.

In the setup, Trigger CLI created a `jobs` directory in our project. We'll create a new file `notification-job.ts` in that directory and add the following code.

```ts
// jobs/notification-job.ts
import { client } from "@/trigger";
import { eventTrigger, retry } from "@trigger.dev/sdk";
import { z } from "zod";

client.defineJob({
  id: "send-notification",
  name: "Send Notification",
  version: "0.0.1",
  trigger: eventTrigger({
    name: "link.viewed",
    schema: z.object({
      viewId: z.string(),
    }),
  }),
  run: async (payload, io, ctx) => {
    const { viewId } = payload;

    // get file url from document version
    const notification = await io.runTask(
      "send-notification",
      async () => {
        const response = await fetch(
          `${process.env.NEXT_PUBLIC_BASE_URL}/api/send-notification`,
          {
            method: "POST",
            body: JSON.stringify({ viewId }),
            headers: {
              "Content-Type": "application/json",
            },
          }
        );

        if (!response.ok) {
          await io.logger.error("Failed to send notification", { payload });
          return;
        }

        const { message } = (await response.json()) as {
          message: string;
        };

        await io.logger.info("Notification sent", { message, payload });
        return { message };
      },
      { retry: retry.standardBackoff }
    );

    return {
      success: true,
      message: "Successfully sent notification",
    };
  },
});
```

Add an export to the jobs index file, otherwise Trigger won't know about the job. Small detail but even I have forgotten about this and searched for the error for a good hour.

```ts
// jobs/index.ts
export * from "./notification-job";
```

### Bonus: Prevent rogue access to the API route

We have our API route ready, but we don't want to allow anyone to access it. We want to make sure that only our application can access it. We'll use a simple header authentication key to do that.

In the Trigger job, we'll add the header to the request:

```ts
// jobs/notification-job.ts
..
...
const response = await fetch(
  `${process.env.NEXT_PUBLIC_BASE_URL}/api/jobs/send-notification`,
  {
    method: "POST",
    body: JSON.stringify({ viewId }),
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.INTERNAL_API_KEY}`, // <- add the authenication header with a local env variable
    },
  },
);
...
..
```

In the API route, we'll check if the API key matches just before the `try {} catch {}` block:

```ts
// pages/api/send-notification.ts
..
...
// Extract the API Key from the Authorization header
const authHeader = req.headers.authorization;
const token = authHeader?.split(" ")[1]; // Assuming the format is "Bearer [token]"

// Check if the API Key matches
if (token !== process.env.INTERNAL_API_KEY) {
  res.status(401).json({ message: "Unauthorized" });
  return;
}
...
..
```

Make sure you add the `INTERNAL_API_KEY` to your `.env` file.

```bash
# .env
INTERNAL_API_KEY="YOUR_API_KEY"
```

## Conclusion

Voila! We have our asynchronous email notification system ready. We can now send emails to our users asynchronously without impacting user wait time. We can also use Trigger to offload many other tasks from our main application that we don't want the user to wait for.

Thank you for reading. I am Marc, an open-source advocate. I am building [papermark.io](https://www.papermark.io) - the open-source alternative to DocSend.

Keep on coding!

## Help me out!

If you found this article helpful and got to understand Trigger and background tasks a bit better, I would be very happy if you could give us a star! And don't forget to share your thoughts in the comments ❤️

[https://github.com/mfts/papermark](https://github.com/mfts/papermark)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nk9c8ktyv1tf3n6jgbxh.gif)