Resend - How to add an Email API Provider to Novu

Prosper Otemuyiwa - Mar 6 '23 - - Dev Community

TL;DR: There is a popular saying that goes like this: You never really know something until you teach it to someone else.

In this tutorial, I'll show you how I added a new email API provider to Novu. In this scenario, I added Resend.

Introduction to Novu

Novu is an open-source service that provides a unified API and components to send notifications through multiple channels, including In-App, Push, Email, SMS, and Chat.

With Novu, you never need to roll your own in-app notification or manually integrate dozens of communication APIs in your app.

In-app Notification Center

Get Started

Head over to GitHub to clone the Novu repo. Feel free to star the repo while you are at it. 😉

Next, create a new branch from the next branch.

git checkout -b add-resend-email-provider next
Enter fullscreen mode Exit fullscreen mode

Install the packages in the Novu project:

npm install
Enter fullscreen mode Exit fullscreen mode

Set Up New Email Provider

Run the command below to seamlessly create the templates needed to add a new email provider.

npm run generate:provider
Enter fullscreen mode Exit fullscreen mode

It generates a prompt like so:

> generate:provider
> npx hygen provider new

? What type of provider is this? …
❯ EMAIL
  SMS
  PUSH
  CHAT
Enter fullscreen mode Exit fullscreen mode
  • Select EMAIL as the provider type.
  • Add the name of the email API provider. Here, I added resend.

Once you hit Return or Enter on your keyboard, the templates will be generated. You should see them in your terminal like so:

added: providers/resend/.czrc
added: providers/resend/.eslintrc.json
added: providers/resend/.gitignore
added: providers/resend/jest.config.js
added: providers/resend/package.json
added: providers/resend/README.md
added: providers/resend/src/index.ts
added: providers/resend/src/lib/repend.provider.ts
added: providers/resend/src/lib/repend.provider.spec.ts
added: providers/resend/tsconfig.json
added: providers/resend/tsconfig.module.json
Enter fullscreen mode Exit fullscreen mode

These files are located in the /providers/resend directory. Navigate to /providers/resend/src/lib/resend.provider.ts in your editor and you should see a template code to start from:

import {
  ChannelTypeEnum,
  ISendMessageSuccessResponse,
  IEmailOptions,
  IEmailProvider,
} from '@novu/stateless';

export class ResendEmailProvider implements IEmailProvider {
  channelType = ChannelTypeEnum.EMAIL as ChannelTypeEnum.EMAIL;

  constructor(
    private config: {
      apiKey: string;
    }
  ) {
  }

  async sendMessage(
    options: IEmailOptions
  ): Promise<ISendMessageSuccessResponse> {


    return {
      id: 'PLACEHOLDER',
      date: 'PLACEHOLDER'
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

/providers/resend/src/lib/resend.provider.ts

Write Code To Send Email

Thankfully, we have a great template to start. We'll proceed to write code.

Note: I already signed up on Resend & have access to my api key. This is the time to set up an account on your Email provider platform if you have not done so.

I have added the complete code needed below to send emails in this file.

import {
  ChannelTypeEnum,
  ISendMessageSuccessResponse,
  ICheckIntegrationResponse,
  CheckIntegrationResponseEnum,
  IEmailOptions,
  IEmailProvider,
} from '@novu/stateless';
import { Resend } from 'resend';

export class ResendEmailProvider implements IEmailProvider {
  id = 'resend';
  channelType = ChannelTypeEnum.EMAIL as ChannelTypeEnum.EMAIL;
  private resendClient: Resend;

  constructor(
    private config: {
      apiKey: string;
      from: string;
    }
  ) {
    this.resendClient = new Resend(this.config.apiKey);
  }

  async sendMessage(
    options: IEmailOptions
  ): Promise<ISendMessageSuccessResponse> {
    const response: any = await this.resendClient.sendEmail({
      from: options.from || this.config.from,
      to: options.to,
      subject: options.subject,
      text: options.text,
      html: options.html,
      cc: options.cc,
      attachments: options.attachments?.map((attachment) => ({
        filename: attachment?.name,
        content: attachment.file,
      })),
      bcc: options.bcc,
    });

    return {
      id: response.id,
      date: new Date().toISOString(),
    };
  }

  async checkIntegration(
    options: IEmailOptions
  ): Promise<ICheckIntegrationResponse> {
    try {
      await this.resendClient.sendEmail({
        from: options.from || this.config.from,
        to: options.to,
        subject: options.subject,
        text: options.text,
        html: options.html,
        cc: options.cc,
        attachments: options.attachments?.map((attachment) => ({
          filename: attachment?.name,
          content: attachment.file,
        })),
        bcc: options.bcc,
      });

      return {
        success: true,
        message: 'Integrated successfully!',
        code: CheckIntegrationResponseEnum.SUCCESS,
      };
    } catch (error) {
      return {
        success: false,
        message: error?.message,
        code: CheckIntegrationResponseEnum.FAILED,
      };
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Here's a breakdown of the code above:

  • Import resend from the resend library.
import { Resend } from 'resend';
Enter fullscreen mode Exit fullscreen mode

Note: Ensure you install the email API client library in the root of the email provider folder. In this case: /apps/providers/resend, NOT THE ROOT OF THE NOVU PROJECT.

  • Set id = 'resend'. This is an identifier for Novu to recognise this provider in other parts of the project.

  • Initialize the email client and set it in the constructor to accept the api key.

...
private resendClient: Resend;

  constructor(
    private config: {
      apiKey: string;
      from: string;
    }
  ) {
    this.resendClient = new Resend(this.config.apiKey);
  }
...
Enter fullscreen mode Exit fullscreen mode
  • The sendMessage method contains the code needed to send the email and return a response.

  • The checkIntegration method is almost a copy of the sendMessage method. It's needed for testing that the email integration works.

Add Email Provider Logos

We need the new email provider to show up in the Integration store on Novu's dashboard. The dark and light mode logos should be present.

Head over to apps/web/public/static/images/providers/dark and apps/web/public/static/images/providers/light directories and the dark and light mode logos respectively. The name of the logos should be name of the provider. The possible formats are svg and png.

In my case, I added resend.svg.

Light Mode
apps/web/public/static/images/providers/light/resend.svg

Dark Mode
apps/web/public/static/images/providers/dark/resend.svg

Build the UI Integration Store

Don't fret, we're not writing any CSS. 😄

We need to provide Novu with some details about our new email provider for it to build the UI Integration store. Two parts:

  • Create credentials config
  • Add provider configuration to providers list

Create credentials config:

We need to add the credentials that are needed in order to create integration with the provider. Add a config object in libs/shared/src/consts/providers/credentials/provider-credentials.ts like below:

export const resendConfig: IConfigCredentials[] = [
  {
    key: CredentialsKeyEnum.ApiKey,
    displayName: 'API Key',
    type: 'string',
    required: true,
  },
  ...mailConfigBase,
];
Enter fullscreen mode Exit fullscreen mode

provider-credentials.ts

Add Provider Configuration to Providers list:

Add the new email provider data to the list in libs/shared/src/consts/providers/channels/email.ts.

Note: The id is the provider's name, displayName is the provider's name in pascal case, credentials are the one you created in the previous step, logoFileName should be as it was in the adding logo step (with the format type included).


import {
  ...
  resendConfig,
} from '../credentials';

export const emailProviders: IProviderConfig[] = [
...
...
{
    id: EmailProviderIdEnum.Resend,
    displayName: 'Resend',
    channel: ChannelTypeEnum.EMAIL,
    credentials: resendConfig,
    docReference: 'https://resend.com/docs',
    logoFileName: { light: 'resend.svg', dark: 'resend.svg' },
}];
Enter fullscreen mode Exit fullscreen mode

libs/shared/src/consts/providers/channels/email.ts

Add Provider handler in the API

Navigate to apps/api directory.

  • We'll start by adding the provider dependency to the API.

In the step where we ran the command to create the template, the project created a standalone provider package that will be published to NPM. In our development environment, it is not yet published.

In order to use it locally, head over to the package.json located in apps/api/package.json and add it manually to the dependencies list: "@novu/": "^VERSION"

"@novu/resend": "^0.11.0"
Enter fullscreen mode Exit fullscreen mode

apps/api/package.json

The value of VERSION should be the package.json version number at the time.

Once you are done, run the npm build command to take into consideration the change we just made!

  • Next, we'll create the provider handler in the API.

In order to map internally the different providers credentials, we need to add a provider handler that is located in the apps/api/src/app/events/services/mail-service/handlers directory.

Create a resend.handler.ts file in this directory and add the following code to it.

import { ChannelTypeEnum } from '@novu/shared';
import { ICredentials } from '@novu/dal';
import { ResendEmailProvider } from '@novu/resend';
import { BaseHandler } from './base.handler';

export class ResendHandler extends BaseHandler {
  constructor() {
    super('resend', ChannelTypeEnum.EMAIL);
  }
  buildProvider(credentials: ICredentials, from?: string) {
    const config: { apiKey: string; from: string } = { from: from as string, apiKey: credentials.apiKey as string };

    this.provider = new ResendEmailProvider(config);
  }
}
Enter fullscreen mode Exit fullscreen mode

resend.handler.ts

  • Export the recently recreated handler in the index.ts file (still in the handlers directory)
...
export * from './resend.handler';
Enter fullscreen mode Exit fullscreen mode

handlers/index.ts

  • Finally, we'll add the handler to the factory.

The factory is located in apps/api/src/app/events/services/mail-service/mail.factory.ts.

Add the Resend Handler like so:

import { IntegrationEntity } from '@novu/dal';
import {
  ...
  ...
  ResendHandler,
} from './handlers';

import { IMailHandler } from './interfaces/send.handler.interface';

export class MailFactory {
  handlers: IMailHandler[] = [
    ...
    ...
    new ResendHandler(),
  ];

  ...
}
Enter fullscreen mode Exit fullscreen mode

apps/api/src/app/events/services/mail-service/mail.factory.ts

Build & Run Novu

To run the project successfully, build from the root of the Novu directory:

pnpm build
Enter fullscreen mode Exit fullscreen mode
  • Navigate to /apps/api and run “pnpm start” in one terminal
  • Navigate to /apps/web and run “pnpm start” in another terminal.

You should be able to login, activate the new email provider and connect successfully.

A Call To Contribute to Open Source

Here's my PR to add Resend to Novu.

Personally, open source has changed my life and career tremendously. So, if you are:

  • Looking for ways to hone your skills,
  • Looking for your first OSS project,
  • Experienced and want to do something meaningful in OSS,
  • Want to contribute to OSS

..then I recommend you contribute to Novu.

Conclusion

I hope you enjoyed following this guide as much as I enjoyed writing it. It covers everything you need to know to add email providers to Novu. This flow can also be adopted to add SMS & Push notification providers.

Novu provides the simplest and easiest way to help developers manage multi-channel notifications with a single API.

One more thing. I'll like to know how you're currently handling sending notifications via multiple channels to your users. Please, let me know in the comments section below!

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .