- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
- Get Started
- Product
- Resources
- Tools & SDKs
- Framework
- Reference
Integrate Medusa with Resend
In this guide, you'll learn how to integrate Medusa with Resend.
When you install a Medusa application, you get a fully-fledged commerce platform with a framework for customization. Medusa's architecture supports integrating third-party services, such as an email service, that allow you to build your unique requirements around core commerce flows.
Resend is an email service with an intuitive developer experience to send emails from any application type, including Node.js servers. By integrating Resend with Medusa, you can build flows to send an email when a commerce operation is performed, such as when an order is placed.
This guide will teach you how to:
- Install and set up Medusa.
- Integrate Resend into Medusa for sending emails.
- Build a flow to send an email with Resend when a customer places an order.
You can follow this guide whether you're new to Medusa or an advanced Medusa developer.
Step 1: Install a Medusa Application#
Start by installing the Medusa application on your machine with the following command:
You'll first be asked for the project's name. Then, when you're asked whether you want to install the Next.js storefront, choose Y
for yes.
Afterwards, the installation process will start, which will install the Medusa application in a directory with your project's name, and the Next.js storefront in a directory with the {project-name}-storefront
name.
Once the installation finishes successfully, the Medusa Admin dashboard will open with a form to create a new user. Enter the user's credential and submit the form. Afterwards, you can login with the new user and explore the dashboard.
The Next.js storefront is also running at http://localhost:8000
.
Step 2: Prepare Resend Account#
If you don't have a Resend Account, create one on their website.
In addition, Resend allows you to send emails from the address onboarding@resend.dev
only to your account's email, which is useful for development purposes. If you have a custom domain to send emails from, add it to your Resend account's domains:
- Go to Domains from the sidebar.
- Click on Add Domain.
3. In the form that opens, enter your domain name and select a region close to your users, then click Add.
4. In the domain's details page that opens, you'll find DNS records to add to your DNS provider. After you add them, click on Verify DNS Records. You can start sending emails from your custom domain once it's verified.
You also need an API key to connect to your Resend account from Medusa, but you'll create that one in a later section.
Step 3: Install Resend Dependencies#
In this step, you'll install two packages useful for your Resend integration:
resend
, which is the Resend SDK:
2. react-email, which is a package created by Resend to create email templates with React:
You'll use these packages in the next steps.
Step 4: Create Resend Module Provider#
To integrate third-party services into Medusa, you create a custom module. A module is a re-usable package with functionalities related to a single feature or domain. Medusa integrates the module into your application without implications or side effects on your setup.
Medusa's Notification Module delegates sending notifications to other modules, called module providers. In this step, you'll create a Resend Module Provider that implements sending notifications through the email channel. In later steps, you'll send email notifications with Resend when an order is placed through this provider.
Create Module Directory#
A module is created under the src/modules
directory of your Medusa application. So, create the directory src/modules/resend
.
Create Service#
You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service.
In this section, you'll create the Resend Module Provider's service and the methods necessary to send an email with Resend.
Start by creating the file src/modules/resend/service.ts
with the following content:
1import { 2 AbstractNotificationProviderService3} from "@medusajs/framework/utils"4import { 5 Logger6} from "@medusajs/framework/types";7import { 8 Resend9} from "resend";10 11type ResendOptions = {12 api_key: string13 from: string14 html_templates?: Record<string, {15 subject?: string16 content: string17 }>18}19 20class ResendNotificationProviderService extends AbstractNotificationProviderService {21 static identifier = "notification-resend"22 private resendClient: Resend23 private options: ResendOptions24 private logger: Logger25 26 // ...27}28 29export default ResendNotificationProviderService
A Notification Module Provider's service must extend the AbstractNotificationProviderService
. It has a send
method that you'll implement soon. The service must also have an identifier
static property, which is a unique identifier that the Medusa application will use to register the provider in the database.
The ResendNotificationProviderService
class also has the following properties:
resendClient
of typeResend
(from the Resend SDK you installed in the previous step) to send emails through Resend.options
of typeResendOptions
. Modules accept options through Medusa's configurations. This ensures that the module is reusable across applications and you don't use sensitive variables like API keys directly in your code. The options that the Resend Module Provider accepts are:api_key
: The Resend API key.from
: The email address to send the emails from.html_templates
: An optional object to replace the default subject and template that the Resend Module uses. This is also useful to support custom emails in different Medusa application setups.
logger
property, which is an instance of Medusa's Logger, to log messages.
To send requests using the resendClient
, you need to initialize it in the class's constructor. So, add the following constructor to ResendNotificationProviderService
:
1// ...2 3type InjectedDependencies = {4 logger: Logger5}6 7class ResendNotificationProviderService extends AbstractNotificationProviderService {8 // ...9 constructor(10 { logger }: InjectedDependencies, 11 options: ResendOptions12 ) {13 super()14 this.resendClient = new Resend(options.api_key)15 this.options = options16 this.logger = logger17 }18}
A module's service accepts two parameters:
- Dependencies resolved from the Module's container, which is the module's local registry that the Medusa application adds framework tools to. In this service, you resolve the Logger utility from the module's container.
- The module's options that are passed to the module in Medusa's configuration as you'll see in a later section.
Using the API key passed in the module's options, you initialize the Resend client. You also set the options
and logger
properties.
Validate Options Method
A Notification Module Provider's service can implement a static validateOptions
method that ensures the options passed to the module through Medusa's configurations are valid.
So, add to the ResendNotificationProviderService
the validateOptions
method:
1// other imports...2import { 3 // other imports...4 MedusaError5} from "@medusajs/framework/utils"6 7// ...8 9class ResendNotificationProviderService extends AbstractNotificationProviderService {10 // ...11 static validateOptions(options: Record<any, any>) {12 if (!options.api_key) {13 throw new MedusaError(14 MedusaError.Types.INVALID_DATA,15 "Option `api_key` is required in the provider's options."16 )17 }18 if (!options.from) {19 throw new MedusaError(20 MedusaError.Types.INVALID_DATA,21 "Option `from` is required in the provider's options."22 )23 }24 }25}
In the validateOptions
method, you throw an error if the api_key
or from
options aren't passed to the module. To throw errors, you use Medusa's MedusaError
utility imported from @medusajs/framework/utils
. This ensures errors follow Medusa's conventions and are displayed similar to Medusa's errors.
Implement Template Methods
Each email type has a different template and content. For example, order confirmation emails show the order's details, whereas customer confirmation emails show a greeting message to the customer.
So, add two methods to the ResendNotificationProviderService
class that retrieve the email template and subject of a specified template type:
1// imports and types...2 3enum Templates {4 ORDER_PLACED = "order-placed",5}6 7const templates: {[key in Templates]?: (props: unknown) => React.ReactNode} = {8 // TODO add templates9}10 11class ResendNotificationProviderService extends AbstractNotificationProviderService {12 // ...13 getTemplate(template: Templates) {14 if (this.options.html_templates?.[template]) {15 return this.options.html_templates[template].content16 }17 const allowedTemplates = Object.keys(templates)18 19 if (!allowedTemplates.includes(template)) {20 return null21 }22 23 return templates[template]24 }25 26 getTemplateSubject(template: Templates) {27 if (this.options.html_templates?.[template]?.subject) {28 return this.options.html_templates[template].subject29 }30 switch(template) {31 case Templates.ORDER_PLACED:32 return "Order Confirmation"33 default:34 return "New Email"35 }36 }37}
You first define a Templates
enum, which holds the names of supported template types. You can add more template types to this enum later. You also define a templates
variable that specifies the React template for each template type. You'll add templates to this variable later.
In the ResendNotificationProviderService
you add two methods:
getTemplate
: Retrieve the template of a template type. If thehtml_templates
option is set for the specified template type, you return itscontent
's value. Otherwise, you retrieve the template from thetemplates
variable.getTemplateSubject
: Retrieve the subject of a template type. If asubject
is passed for the template type in thehtml_templates
, you return its value. Otherwise, you return a subject based on the template type.
You'll use these methods in the send
method next.
Implement Send Method
In this section, you'll implement the send
method of ResendNotificationProviderService
. When you send a notification through the email channel later using the Notification Module, the Notification Module's service will use this send
method under the hood to send the email with Resend.
In the send
method, you'll retrieve the template and subject of the email template, then send the email using the Resend client.
Add the send
method to the ResendNotificationProviderService
class:
1// other imports...2import { 3 // ...4 ProviderSendNotificationDTO, 5 ProviderSendNotificationResultsDTO,6} from "@medusajs/framework/types"7import { 8 // ...9 CreateEmailOptions, 10} from "resend"11 12class ResendNotificationProviderService extends AbstractNotificationProviderService {13 // ...14 async send(15 notification: ProviderSendNotificationDTO16 ): Promise<ProviderSendNotificationResultsDTO> {17 const template = this.getTemplate(notification.template as Templates)18 19 if (!template) {20 this.logger.error(`Couldn't find an email template for ${notification.template}. The valid options are ${Object.values(Templates)}`)21 return {}22 }23 24 const emailOptions: CreateEmailOptions = {25 from: this.options.from,26 to: [notification.to],27 subject: this.getTemplateSubject(notification.template as Templates),28 html: ""29 }30 31 if (typeof template === "string") {32 emailOptions.html = template33 } else {34 emailOptions.react = template(notification.data)35 delete emailOptions.html36 }37 38 const { data, error } = await this.resendClient.emails.send(emailOptions)39 40 if (error) {41 this.logger.error(`Failed to send email`, error)42 return {}43 }44 45 return { id: data.id }46 }47}
The send
method receives the notification details object as a parameter. Some of its properties include:
to
: The address to send the notification to.template
: The template type of the notification.data
: The data useful for the email type. For example, when sending an order-confirmation email,data
would hold the order's details.
In the method, you retrieve the template and subject of the email using the methods you defined earlier. Then, you put together the data to pass to Resend, such as the email address to send the notification to and the email address to send from. Also, if the email's template is a string, it's passed as an HTML template. Otherwise, it's passed as a React template.
Finally, you use the emails.send
method of the Resend client to send the email. If an error occurs you log it in the terminal. Otherwise, you return the ID of the send email as received from Resend. Medusa uses this ID when creating the notification in its database.
Export Module Definition#
The ResendNotificationProviderService
class now has the methods necessary to start sending emails.
Next, you must export the module provider's definition, which lets Medusa know what module this provider belongs to and its service.
Create the file src/modules/resend/index.ts
with the following content:
You export the module provider's definition using the ModuleProvider
utility function. It accepts as a first parameter the name of the module that this provider belongs to, which is the Notification Module. It also accepts as a second parameter an object having a service
property indicating the provider's service.
Add Module to Configurations#
Finally, to register modules and module providers in Medusa, you must add them to Medusa's configurations.
Medusa's configurations are set in the medusa-config.ts
file, which is at the root directory of your Medusa application. The configuration object accepts a modules
array, whose value is an array of modules to add to the application.
Add the modules
property to the exported configurations in medusa-config.ts
:
1module.exports = defineConfig({2 // ...3 modules: [4 {5 resolve: "@medusajs/medusa/notification",6 options: {7 providers: [8 {9 resolve: "./src/modules/resend",10 id: "resend",11 options: {12 channels: ["email"],13 api_key: process.env.RESEND_API_KEY,14 from: process.env.RESEND_FROM_EMAIL,15 },16 },17 ],18 },19 },20 ],21})
In the modules
array, you pass a module object having the following properties:
resolve
: The NPM package of the Notification Module. Since the Resend Module is a Notification Module Provider, it'll be passed in the options of the Notification Module.options
: An object of options to pass to the module. It has aproviders
property which is an array of module providers to register. Each module provider object has the following properties:resolve
: The path to the module provider to register in the application. It can also be the name of an NPM package.id
: A unique ID, which Medusa will use along with theidentifier
static property that you set earlier in the class to identify this module provider.options
: An object of options to pass to the module provider. These are the options you expect and use in the module provider's service. You must also specify thechannels
option, which indicates the channels that this provider sends notifications through.
Some of the module's options, such as the Resend API key, are set in environment variables. So, add the following environment variables to .env
:
Where:
RESEND_FROM_EMAIL
: The email to send emails from. If you've configured the custom domain as explained in Step 2, change this email to an email from your custom domain. Otherwise, you can useonboarding@resend.dev
for development purposes.RESEND_API_KEY
is the API key of your Resend account. To retrieve it:- Go to API Keys in the sidebar.
- Click on the Create API Key button.
- In the form that opens, enter a name for the API key (for example, Medusa). You can keep its permissions to Full Access or change it to Sending Access. Once you're done, click Add.
- A new pop-up will show with your API key hidden. Copy it before closing the pop-up, since you can't access the key again afterwards. Use its value for the
RESEND_API_KEY
environment variable.
Your Resend Module Provider is all set up. You'll test it out in a later section.
Step 5: Add Order Confirmation Template#
In this step, you'll add a React template for order confirmation emails. You'll create it using the react-email package you installed earlier. You can follow the same steps for other email templates, such as for customer confirmation.
Create the directory src/modules/resend/emails
that will hold the email templates. Then, to add the template for order confirmation, create the file src/modules/resend/emails/order-placed.tsx
with the following content:
1import { Text, Column, Container, Heading, Html, Img, Row, Section } from "@react-email/components"2import { BigNumberValue, OrderDTO } from "@medusajs/framework/types"3 4type OrderPlacedEmailProps = {5 order: OrderDTO6}7 8function OrderPlacedEmailComponent({ order }: OrderPlacedEmailProps) {9 const formatter = new Intl.NumberFormat([], {10 style: "currency",11 currencyDisplay: "narrowSymbol",12 currency: order.currency_code,13 })14 15 const formatPrice = (price: BigNumberValue) => {16 if (typeof price === "number") {17 return formatter.format(price)18 }19 20 if (typeof price === "string") {21 return formatter.format(parseFloat(price))22 }23 24 return price?.toString() || ""25 }26 27 return (28 <Html>29 <Heading>Thank you for your order</Heading>30 {order.email}'s Items31 <Container>32 {order.items.map(item => {33 return (34 <Section35 key={item.id}36 style={{ paddingTop: "40px", paddingBottom: "40px" }}37 >38 <Row>39 <Column>40 <Img41 src={item.thumbnail}42 alt={item.product_title}43 style={{ float: "left" }}44 width="260px"45 />46 </Column>47 <Column style={{ verticalAlign: "top", paddingLeft: "12px" }}>48 <Text style={{ fontWeight: "500" }}>49 {item.product_title}50 </Text>51 <Text>{item.variant_title}</Text>52 <Text>{formatPrice(item.total)}</Text>53 </Column>54 </Row>55 </Section>56 )57 })}58 </Container>59 </Html>60 )61}62 63export const orderPlacedEmail = (props: OrderPlacedEmailProps) => (64 <OrderPlacedEmailComponent {...props} />65)
You define the OrderPlacedEmailComponent
which is a React email template that shows the order's details, such as items and their totals. The component accepts an order
object as a prop.
You also export an orderPlacedEmail
function, which accepts props as an input and returns the OrderPlacedEmailComponent
passing it the props. Because you can't use JSX syntax in src/modules/resend/service.ts
, you'll import this function instead.
Next, update the templates
variable in src/modules/resend/service.ts
to assign this template to the order-placed
template type:
The ResendNotificationProviderService
will now use the OrderPlacedEmailComponent
as the template of order confirmation emails.
Step 6: Send Email when Order is Placed#
Medusa has an event system that emits an event when a commerce operation is performed. You can then listen and handle that event in an asynchronous function called a subscriber.
So, to send a confirmation email when a customer places an order, which is a commerce operation that Medusa already implements, you don't need to extend or hack your way into Medusa's implementation as you would do with other commerce platforms.
Instead, you'll create a subscriber that listens to the order.placed
event and sends an email when the event is emitted.
Send Order Confirmation Email Workflow#
To send the order confirmation email, you need to retrieve the order's details first, then use the Notification Module's service to send the email. To implement this flow, you'll create a workflow.
A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow like you construct a function, but it's a special function that allows you to track its executions' progress, define roll-back logic, and configure other advanced features. Then, you execute the workflow from other customizations, such as in a subscriber.
Send Notification Step
You'll start by implementing the step of the workflow that sends the notification. To do that, create the file src/workflows/steps/send-notification.ts
with the following content:
1import { Modules } from "@medusajs/framework/utils"2import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"3import { CreateNotificationDTO } from "@medusajs/framework/types"4 5export const sendNotificationStep = createStep(6 "send-notification",7 async (data: CreateNotificationDTO[], { container }) => {8 const notificationModuleService = container.resolve(9 Modules.NOTIFICATION10 )11 const notification = await notificationModuleService.createNotifications(data)12 return new StepResponse(notification)13 }14)
You define the sendNotificationStep
using the createStep
function that accepts two parameters:
- A string indicating the step's unique name.
- The step's function definition as a second parameter. It accepts the step's input as a first parameter, and an object of options as a second.
The container
property in the second parameter is an instance of the Medusa container, which is a registry of framework and commerce tools, such a module's service, that you can resolve to utilize their functionalities.
In the step function, you resolve the Notification Module's service, and use its createNotifications
method, passing it the notification's data that the step receives as an input.
The step returns an instance of StepResponse
, which must be returned by any step. It accepts as a parameter the data to return to the workflow that executed this step.
Workflow Implementation
You'll now create the workflow that uses the sendNotificationStep
to send the order confirmation email.
Create the file src/workflows/send-order-confirmation.ts
with the following content:
1import { 2 createWorkflow, 3 WorkflowResponse4} from "@medusajs/framework/workflows-sdk";5import { useQueryGraphStep } from "@medusajs/medusa/core-flows";6import { sendNotificationStep } from "./steps/send-notification";7 8type WorkflowInput = {9 id: string10}11 12export const sendOrderConfirmationWorkflow = createWorkflow(13 "send-order-confirmation",14 ({ id }: WorkflowInput) => {15 // @ts-ignore16 const { data: orders } = useQueryGraphStep({17 entity: "order",18 fields: [19 "id",20 "email",21 "currency_code",22 "total",23 "items.*",24 ],25 filters: {26 id27 }28 })29 30 const notification = sendNotificationStep([{31 to: orders[0].email,32 channel: "email",33 template: "order-placed",34 data: {35 order: orders[0]36 }37 }])38 39 return new WorkflowResponse(notification)40 }41)
You create a workflow using the createWorkflow
function imported from @medusajs/framework/workflows-sdk
. It accepts the workflow's unique name as a first parameter.
It accepts as a second parameter a constructor function, which is the workflow's implementation. The workflow has the following steps:
useQueryGraphStep
, which is a step implemented by Medusa that uses Query, a tool that allows you to retrieve data across modules. You use it to retrieve the order's details.sendNotificationStep
which is the step you implemented. You pass it an array with one object, which is the notification's details having following properties:to
: The address to send the email to. You pass the customer's email that is stored in the order.channel
: The channel to send the notification through, which isemail
. Since you specifiedemail
in the Resend Module Provider'schannel
option, the Notification Module will delegate the sending to the Resend Module Provider's service.template
: The email's template type. You retrieve the template content in theResendNotificationProviderService
'ssend
method based on the template specified here.data
: The data to pass to the email template, which is the order's details.
You'll execute the workflow when you create the subscriber next.
Add the Order Placed Subscriber
Now that you have the workflow to send an order-confirmation email, you'll execute it in a subscriber that's executed whenever an order is placed.
You create a subscriber in a TypeScript or JavaScript file under the src/subscribers
directory. So, create the file src/subscribers/order-placed.ts
with the following content:
1import type {2 SubscriberArgs,3 SubscriberConfig,4} from "@medusajs/framework"5import { sendOrderConfirmationWorkflow } from "../workflows/send-order-confirmation"6 7export default async function orderPlacedHandler({8 event: { data },9 container,10}: SubscriberArgs<{ id: string }>) {11 await sendOrderConfirmationWorkflow(container)12 .run({13 input: {14 id: data.id15 }16 })17}18 19export const config: SubscriberConfig = {20 event: "order.placed",21}
A subscriber file exports:
- An asynchronous function that's executed whenever the associated event is emitted, which is the
order.placed
event. - A configuration object with an
event
property indicating the event the subscriber is listening to.
The subscriber function accepts the event's details as a first paramter which has a data
property that holds the data payload of the event. For example, Medusa emits the order.placed
event with the order's ID in the data payload. The function also accepts as a second parameter the Medusa container.
In the function, you execute the sendOrderConfirmationWorkflow
by invoking it, passing it the container
, then using its run
method. The run
method accepts an object having an input
property, which is the input to pass to the workflow. You pass the ID of the placed order as received in the event's data payload.
This subscriber now runs whenever an order is placed. You'll see this in action in the next section.
Test it Out: Place an Order#
To test out the Resend integration, you'll place an order using the Next.js storefront that you installed as part of installing Medusa.
Start your Medusa application first:
Then, in the Next.js storefront's directory (which was installed in a directory outside of the Medusa application's directory with the name {project-name}-storefront
, where {project-name}
is the name of the Medusa application's directory), run the following command to start the storefront:
Then, open the storefront in your browser at http://localhost:8000
and:
- Go to Menu -> Store.
2. Click on a product, select its options, and add it to the cart.
3. Click on Cart at the top right, then click Go to Cart.
4. On the cart's page, click on the "Go to checkout" button.
5. On the checkout page, when entering the shipping address, make sure to set the email to your Resend account's email if you didn't set up a custom domain.
6. After entering the shipping address, choose a delivery and payment methods, then click the Place Order button.
Once the order is placed, you'll find the following message logged in the Medusa application's terminal:
This indicates that the order.placed
event was emitted and its subscriber, which you added in the previous step, is executed.
If you check the inbox of the email address you specified in the shipping address, you'll find a new email with the order's details.
Next Steps#
You've now integrated Medusa with Resend. You can add more templates for other emails, such as customer registration confirmation, user invites, and more. Check out the Events Reference for a list of all events that the Medusa application emits.
If you're new to Medusa, check out the main documentation, where you'll get a more in-depth learning of all the concepts you've used in this guide and more.
To learn more about the commerce features that Medusa provides, check out Medusa's Commerce Modules.
For other general guides related to deployment, storefront development, integrations, and more, check out the Development Resources.