Building a Blog Subscription and Pusher with AirCode and Resend

0xinhua 发布于

Introduction

Learn how to build subscription and push notification services in Node.js and Next.js, and send your first email using the Resend Node.js SDK on AirCode.

Here's what the finished page and email will look like:

subscribe-form-email.png

In this tutorial, I'll guide you through how I used AirCode and Resend to add basic subscription and email delivery features to a blog.

You'll learn:

You can also directly check out the full source code on GitHub so you can get started fast!

About AirCode

Just a quick background about the platform:

Serverless Node.js stack for API development, AirCode is an innovative online platform designed to supercharge developers in building APIs with Node.js.

No credit card is needed, deploy your code on AirCode. => https://aircode.io

Prerequisites

To get the most out of this guide, you’ll need two accounts, no worries, both of these are available in free plan:

  • AirCode Serverless Node.js stack for API development

  • Resend Email service for developers, prepare an API key and verify your domain

Crafting the user interface

Before diving into the tutorial, let's briefly review the subscribe and push system. What are the functional requirements?

  • An input field and button to submit a user's email

  • An API endpoint to save data and communicate with the front-end

  • An updatable email template for inserting dynamic post content

  • An email delivery system for notifications

First, we'll create the user subscription interface for email input on our blog pages, I will use Next.js and Tailwind CSS for the interface.

Let's set it up.

Using create-next-app to create a subscribe-form folder for the web application as done below:

sh
1npx create-next-app@latest

On installation, you'll see the following prompts:

sh
1npx create-next-app@latest 2✔ What is your project named? … subscribe-form 3✔ Would you like to use TypeScript? … No / Yes 4✔ Would you like to use ESLint? … No / Yes 5✔ Would you like to use Tailwind CSS? … No / Yes 6✔ Would you like to use `src/` directory? … No / Yes 7✔ Would you like to use App Router? (recommended) … No / Yes 8✔ Would you like to customize the default import alias? … No / Yes

After Initialized git and Installing dependencies, Congratulations! 🎉 You can now start the app by using the command below.

sh
1npm run dev

Find the page.tsx in src/app/page.tsx, copy the code snippet below to replace the default page content:

jsx
1"use client" 2 3import { useState } from 'react' 4 5export default function Home() { 6 7 const onSubscribe = async (_e) => {} 8 9 const [email, setEmail] = useState('') 10 const [message, setMessage] = useState('') 11 12 const onChange = (email: string): void => { 13 setEmail(email) 14 if (message) { 15 setMessage('') 16 } 17 } 18 return ( 19 <main className="flex min-h-screen flex-col items-center justify-between p-24 bg-black"> 20 <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex"> 21 <div className="py-16 sm:py-24 lg:py-32"> 22 <div className="mx-auto grid max-w-7xl grid-cols-1 gap-10 px-6 lg:grid-cols-12 lg:gap-8 lg:px-8"> 23 <div className="max-w-2xl text-3xl font-bold tracking-tight text-neutral-100 sm:text-4xl lg:col-span-7"> 24 <p className="inline sm:block lg:inline xl:block">Want product news and updates?</p>{' '} 25 <p className="inline sm:block lg:inline xl:block">Sign up for our newsletter.</p> 26 </div> 27 <form className="w-full max-w-md lg:col-span-5 lg:pt-2" onSubmit={onSubscribe}> 28 <div className="flex gap-x-4"> 29 <label htmlFor="email-address" className="sr-only"> 30 Email address 31 </label> 32 <input 33 id="email-address" 34 name="email" 35 type="email" 36 autoComplete="email" 37 required 38 className="min-w-0 flex-auto rounded-md border-0 bg-neutral-100/5 px-3.5 py-2 text-neutral-100 shadow-sm ring-1 ring-inset ring-neutral-100/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm sm:leading-6" 39 placeholder="Enter your email" 40 value={email} 41 onChange={(e) => onChange(e.target.value)} 42 /> 43 <button 44 type="submit" 45 className="flex-none rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-neutral-100 shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500" 46 > 47 Subscribe 48 </button> 49 </div> 50 <div className='mt-2.5 leading-6'> 51 { <span className='text-[13px] block text-[#8a8f98] font-medium'>{ message }</span> } 52 </div> 53 <p className="mt-4 text-sm leading-6 text-neutral-300"> 54 We care about your data. Read our{' '} 55 <a href="https://docs.aircode.io/legal/privacy-policy" className="font-semibold text-neutral-100"> 56 Privacy&nbsp;Policy 57 </a> 58 . 59 </p> 60 </form> 61 </div> 62 </div> 63 </div> 64 </main> 65 ) 66}

Save the code, then you will see an elegant subscribe form like this:

subscribe-form.png

Communicating with Node.js API in AirCode

Create an AirCode App

In this section, you'll learn how to communicate with your Node.js server by creating an API in AirCode.

Before we start coding, log in to aircode.io/login and create a new app. Input an app name and select the TypeScript option:

image.png

After entering the dashboard page,

  • Change the default hello.ts file name with subscribe.ts.

  • Click the Deploy button deploy your first API in the second.

deploy.gif

Copy the invoke url into the browser, now you have your first interactive RESTful API.

sh
1https://byq3nrmbgm.us.aircode.run/subscribe

Submit Email

Back to the front end, when submitting the subscription form, we'll send the form data to the server. Let's add some code.

In a Next.js client component, if you need to fetch data, you can call a Route Handler. Next.js extends the native fetch Web API, allowing you to configure caching and revalidation behavior for each fetch request on the server. Alternatively, you can use a third-party library for requesting. In this case, I'm using SWR as recommended in the documentation.

Use the following shell to install swr:

sh
1npm install swr

Copy the following code in src/app/page.tsx file, the following code is used to request the backend API:

js
1import { useRef, useState } from 'react' 2import useSWRMutation from 'swr/mutation' 3 4 const emailRef = useRef<HTMLInputElement>() 5 const [email, setEmail] = useState('') 6 const [message, setMessage] = useState('') 7 8 const onChange = (email: string): void => { 9 setEmail(email) 10 if (message) { 11 setMessage('') 12 } 13 } 14 15 async function sendRequest(url: string, { arg }: { arg: { email: string }}) { 16 return fetch(url, { 17 headers: { 18 'Content-Type': 'application/json' 19 }, 20 method: 'POST', 21 body: JSON.stringify(arg) 22 }).then(res => res.json()) 23 } 24 25 // replace with your invoke url you got in the previous step 26 const { trigger, isMutating } = useSWRMutation('https://byq3nrmbgm.us.aircode.run/subscribe', sendRequest, /* options */) 27 28 const subscribe = async (e) => { 29 e.preventDefault(); 30 if(!email && emailRef.current) { 31 emailRef.current.focus() 32 setMessage('Please fill out email field.') 33 return 34 } 35 try { 36 const result = await trigger({ email }, /* options */) 37 console.log('subscribe result: ', result) 38 39 const { message, code } = result 40 if (message) { 41 setMessage(result?.message) 42 } 43 44 if (code === 0) { 45 setEmail('') 46 } 47 48 } catch (e) { 49 let message = 'An error has occurred. ' 50 if (e?.message) { 51 message += `error message: ${e.message}. `; 52 } 53 message += 'please try again later.' 54 setMessage(message) 55 } 56 };

Here is the full code of src/app/page.tsx, including the form UI and API request code:

javascript
1"use client" 2 3import { useRef, useState } from 'react' 4import useSWRMutation from 'swr/mutation' 5 6async function sendRequest(url: string, { arg }: { arg: { email: string }}) { 7 return fetch(url, { 8 headers: { 9 'Content-Type': 'application/json' 10 }, 11 method: 'POST', 12 body: JSON.stringify(arg) 13 }).then(res => res.json()) 14} 15 16export default function Home() { 17 18 const emailRef = useRef<HTMLInputElement>() 19 const [email, setEmail] = useState('') 20 const [message, setMessage] = useState('') 21 22 // repalce with your own endpoint url 23 const { trigger } = useSWRMutation('https://byq3nrmbgm.us.aircode.run/subscribe', sendRequest, /* options */) 24 25 const onSubscribe = async (_e: React.FormEvent<HTMLFormElement>) => { 26 _e.preventDefault(); 27 if(!email && emailRef.current) { 28 emailRef.current.focus() 29 setMessage('Please fill out email field.') 30 return 31 } 32 try { 33 const result = await trigger({ email }, /* options */) as { 34 message: string, 35 code: number 36 } 37 38 const { message, code } = result 39 if (message) { 40 setMessage(result?.message) 41 } 42 43 if (code === 0) { 44 setEmail('') 45 } 46 47 } catch (error) { 48 const e = error as { message: string } 49 let message = 'An error has occurred. ' 50 if (e && 'message' in e) { 51 message += `error message: ${e.message}. ` 52 } 53 message += 'please try again later.' 54 setMessage(message) 55 } 56 } 57 58 const onChange = (email: string): void => { 59 setEmail(email) 60 if (message) { 61 setMessage('') 62 } 63 } 64 return ( 65 <main className="flex min-h-screen flex-col items-center justify-between p-24 bg-black"> 66 <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex"> 67 <div className="py-16 sm:py-24 lg:py-32"> 68 <div className="mx-auto grid max-w-7xl grid-cols-1 gap-10 px-6 lg:grid-cols-12 lg:gap-8 lg:px-8"> 69 <div className="max-w-2xl text-3xl font-bold tracking-tight text-neutral-100 sm:text-4xl lg:col-span-7"> 70 <p className="inline sm:block lg:inline xl:block">Want product news and updates?</p>{' '} 71 <p className="inline sm:block lg:inline xl:block">Sign up for our newsletter.</p> 72 </div> 73 <form className="w-full max-w-md lg:col-span-5 lg:pt-2" onSubmit={onSubscribe}> 74 <div className="flex gap-x-4"> 75 <label htmlFor="email-address" className="sr-only"> 76 Email address 77 </label> 78 <input 79 id="email-address" 80 name="email" 81 type="email" 82 autoComplete="email" 83 required 84 className="min-w-0 flex-auto rounded-md border-0 bg-neutral-100/5 px-3.5 py-2 text-neutral-100 shadow-sm ring-1 ring-inset ring-neutral-100/10 focus:ring-2 focus:ring-inset focus:ring-indigo-500 sm:text-sm sm:leading-6" 85 placeholder="Enter your email" 86 value={email} 87 onChange={(e) => onChange(e.target.value)} 88 /> 89 <button 90 type="submit" 91 className="flex-none rounded-md bg-indigo-500 px-3.5 py-2.5 text-sm font-semibold text-neutral-100 shadow-sm hover:bg-indigo-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500" 92 > 93 Subscribe 94 </button> 95 </div> 96 <div className='mt-2.5 leading-6'> 97 { <span className='text-[13px] block text-[#8a8f98] font-medium'>{ message }</span> } 98 </div> 99 <p className="mt-4 text-sm leading-6 text-neutral-300"> 100 We care about your data. Read our{' '} 101 <a href="https://docs.aircode.io/legal/privacy-policy" className="font-semibold text-neutral-100"> 102 Privacy&nbsp;Policy 103 </a> 104 . 105 </p> 106 </form> 107 </div> 108 </div> 109 </div> 110 </main> 111 ) 112}

You will find a TS error in the module importing:

sh
1Cannot find module 'swr/mutation'. Did you mean to set the 'moduleResolution' option to 'node', 2or to add aliases to the 'paths' option?ts(2792)

This error occurs when TypeScript cannot find the swr/mutation module during compilation. There are a couple of things you can try to resolve it, set moduleResolution to node in your tsconfig.json:

json
1{ 2 "compilerOptions": { 3 "target": "es5", 4 "lib": ["dom", "dom.iterable", "esnext"], 5 "allowJs": true, 6 "skipLibCheck": true, 7 "strict": true, 8 "noEmit": true, 9 "esModuleInterop": true, 10 "module": "esnext", 11 "moduleResolution": "node", 12 "resolveJsonModule": true, 13 "isolatedModules": true, 14 "jsx": "preserve", 15 "incremental": true, 16 "plugins": [ 17 { 18 "name": "next" 19 } 20 ], 21 "paths": { 22 "@/*": ["./src/*"] 23 } 24 }, 25 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 "exclude": ["node_modules"] 27}

From the code snippet above, when user input their email and click the submit button, we use swr/mutation trigger HTTP fetching. You need to replace with your invoke URL you got in the previous step in this line:

js
1// replace with your invoke url got in the previous step after your deploy 2const { trigger, isMutating } = useSWRMutation('https://byq3nrmbgm.us.aircode.run/subscribe', sendRequest, /* options */)

You can try to input an email and then send it to your API server, if you see the following response Hi, AirCode., Congratulations, your first subscribe API is ready.

api-fetch-swr.gif

Integrating next.js with Serverless function

And now, we can let AirCode save our data! Let's enrich our subscribe function!

When we receive a request, We can currently add simple validations:

  • First which must be non-null

  • The passed email parameter must be in the correct email format

  • Third, if the current mailbox has been subscribed, respond correct message

We need to validate the email parameter to check if it's a properly formatted email address before storing it in the database.

js
1// @see https://docs.aircode.io/guide/functions/ 2import aircode from 'aircode'; 3 4const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 5 6export default async function (params: any, context: any) { 7 console.log('Received params:', params, typeof params); 8 9 const { email } = params; 10 11 console.log('email', email); 12 13 if (!email) { 14 return { 15 code: 1, 16 message: 'Email required.', 17 }; 18 } 19 20 if (!regex.test(email)) { 21 return { 22 code: 1, 23 message: 'Invalid email.', 24 }; 25 } 26 27}

The code snippet above accepts a post request from the next.js subscribes App with the user's email, After the simple validation, we need to use the database to store the data.

In AirCode you don't need to set up a MySQL or other NoSQL database, you just need new a table and save your data.

js
1try { 2 // Get the emails table 3 const EmailsTable = aircode.db.table('emails'); 4 5 // Find email by address 6 const matchedRecord = await EmailsTable.where({ email }).findOne(); 7 8 if (matchedRecord) { 9 return { 10 code: 0, 11 message: 'Your email is already in our subscription list.', 12 }; 13 } 14 15 // Insert a new email 16 const newEmail = { 17 email, 18 }; 19 20 await EmailsTable.save(newEmail); 21 22 return { 23 code: 0, 24 message: 'You have been successfully subscribed to our newsletter.', 25 }; 26} catch (err) { 27 return { 28 code: 1, 29 message: `An error occurred while subscribing, please try again later, the error message: ${err}`, 30 }; 31}

From the code snippet above:

  • Create an emails table saving data with aircode.db.table(tableName)

  • Find one matching record through where({ field: value }).findOne(), check whether the user is already subscribed

  • Insert one record at once via Table.save(record), save is an async function, so it needs to use await to ensure that the execution ends.

Create a beautiful email template for blog updates

When comes to building an Email template, It's just not an enjoyable experience, typically, you can only send emails using HTML or plain text, and:

  • You can't see the results in real-time before you send them for testing

  • There may be compatibility issues in the display of various email systems

Thanks @react-email an open source helping build email with React components and Tailwind CSS.

Let's quickly render an email template in AirCode:

Create an email Component

Add a email.jsx for our email template, replace with the following content:

jsx
1const { 2 Body, 3 Container, 4 Column, 5 Hr, 6 Html, 7 Img, 8 Link, 9 Button, 10 Row, 11 Section, 12 Text, 13} = require('@react-email/components'); 14 15const React = require('react'); 16 17const dt = new Date(); 18const year = dt.getFullYear(); 19 20const getEmail = (title, excerpt, coverImage, href) => { 21 return ( 22 <Html> 23 <Body style={main}> 24 <Container style={container}> 25 <Section style={header}> 26 <Link href={href + "?ref=view-in-browser"} style={viewBrowserLink}> 27 View in browser 28 </Link> 29 <Text style={splitLine}>|</Text> 30 <Link href="https://docs.aircode.io/" style={viewBrowserLink}> 31 About AirCode 32 </Link> 33 </Section> 34 <Section style={logo}> 35 <Img 36 style={logoImage} 37 src="https://s2.loli.net/2023/08/23/BKvqVWsig97DuYZ.png" 38 width="35px" 39 height="35px" 40 alt="logo" 41 /> 42 <h2 style={emailTitle}>{title}</h2> 43 </Section> 44 <Section style={paragraphContent}> 45 <Hr style={hr} /> 46 <Text style={heading}>Hi, here are the latest updates: </Text> 47 </Section> 48 49 <Section style={paragraphContent}> 50 <Column> 51 <Text style={paragraph}>{excerpt}</Text> 52 </Column> 53 </Section> 54 55 <Section style={paragraphContent}> 56 <Column style={postImage}> 57 <Img 58 src={coverImage} 59 style={{ borderRadius: "2px" }} 60 alt="What we are building" 61 width="400px" 62 /> 63 </Column> 64 </Section> 65 66 <Section 67 style={{ 68 ...paragraphContent, 69 textAlign: "center", 70 marginTop: "20px", 71 }} 72 > 73 <Button pX={12} pY={12} style={button} href={href + "?ref=read-the-post"}> 74 Read the post 75 </Button> 76 </Section> 77 78 <Section style={{ ...paragraphContent, textAlign: 'center' }}> 79 <Text style={mediaParagraph}>Star and Follow us</Text> 80 </Section> 81 <Section style={containerContact}> 82 <Link 83 style={mediaLink} 84 href="https://github.com/aircodelabs/aircode" 85 > 86 <Img 87 width="28" 88 height="28" 89 src="https://s2.loli.net/2023/08/23/UOrLaKQHWNoJi3v.png" 90 /> 91 </Link> 92 <Link style={mediaLink} href="https://twitter.com/aircode_io"> 93 <Img 94 width="28" 95 height="28" 96 src="https://s2.loli.net/2023/08/23/gr3n1jYGCkJ9oxT.png" 97 /> 98 </Link> 99 </Section> 100 101 <Section style={{ ...paragraphContent, paddingBottom: 30 }}> 102 <Text 103 style={{ 104 ...paragraph, 105 fontSize: '12px', 106 textAlign: 'center', 107 margin: 0, 108 }} 109 > 110 {`©${year} AirCode, Inc. All rights reserved.`} 111 </Text> 112 </Section> 113 </Container> 114 </Body> 115 </Html> 116 ); 117}; 118 119module.exports = getEmail; 120 121const main = { 122 padding: '10px 2px', 123 backgroundColor: '#f5f5f5', 124 fontFamily: 125 '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', 126}; 127 128const header = { 129 color: '#666', 130 padding: '20px 0', 131 textAlign: 'center', 132}; 133 134const logo = { 135 display: 'inline-table', 136 textAlign: 'center', 137 margin: '0 auto', 138}; 139 140const logoImage = { 141 display: 'inline-block', 142 marginRight: '10px', 143}; 144 145const emailTitle = { 146 display: 'inline-block', 147 fontSize: '24px', 148}; 149 150const splitLine = { 151 lineHeight: '10px', 152 margin: '0 4px', 153 display: 'inline-block', 154}; 155 156const viewBrowserLink = { 157 display: 'inline-block', 158 fontSize: '11px', 159 lineHeight: '10px', 160 textUnderlinePosition: 'from-font', 161 textDecoration: 'underline', 162 color: '#666', 163 textDecorationColor: '#666', 164}; 165 166const sectionLogo = { 167 padding: '0 10px', 168}; 169 170const container = { 171 margin: '30px auto', 172 width: '610px', 173 backgroundColor: '#fff', 174 borderRadius: 5, 175 overflow: 'hidden', 176}; 177 178const containerContact = { 179 width: '100%', 180 borderRadius: '5px', 181 overflow: 'hidden', 182 textAlign: 'center', 183 marginBottom: '16px', 184}; 185 186const mediaLink = { 187 display: 'inline-block', 188 textAlign: 'center', 189 margin: '0 5px', 190}; 191 192const postTitle = { 193 marginLeft: '10px', 194 fontSize: '16px', 195 lineHeight: '26px', 196 fontWeight: '700', 197 color: '#6B7AFF', 198}; 199 200const postImage = { 201 paddingTop: '16px', 202}; 203 204const mediaParagraph = { 205 fontSize: '12px', 206 lineHeight: '20px', 207 color: '#3c4043', 208}; 209 210const heading = { 211 fontSize: '14px', 212 lineHeight: '26px', 213}; 214 215const button = { 216 backgroundColor: '#6B7AFF', 217 borderRadius: '3px', 218 color: '#fff', 219 textDecoration: 'none', 220 textAlign: 'center', 221 display: 'block', 222 marginTop: '26px', 223}; 224 225const paragraphContent = { 226 padding: '0 40px', 227}; 228 229const paragraph = { 230 fontSize: '14px', 231 lineHeight: '22px', 232 color: '#3c4043', 233}; 234 235const hr = { 236 borderColor: '#e8eaed', 237 margin: '20px 0', 238};

Render the component to an HTML string

Add a render.ts function to convert components to string text content, the code:

ts
1// https://react.email/docs/utilities/render 2 3require('@babel/register')({ 4 presets: ['@babel/preset-react'], 5}); 6 7import aircode from 'aircode'; 8const getEmail = require('./email.jsx'); 9import { render } from '@react-email/render'; 10 11// test post data 12const post = { 13 href: "https://aircode.io/blog/why-create-aircode", 14 title: "What we are building", 15 excerpt: `AirCode is Your Serverless Node.js Stack for API Development, 16 zero-config, all in one place.AirCode is Your Serverless Node.js 17 Stack for API Development, zero-config, all in one place`, 18 coverImage: 19 "https://ph-files.imgix.net/b41dc780-1623-4c46-90b9-1a0d514c5730.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&fit=max&dpr=2", 20}; 21 22export default async function (params: any, context: any) { 23 const { title, excerpt, coverImage, href } = post; 24 const html = render(getEmail(title, excerpt, coverImage, href)); 25 console.log('render email template: ', html); 26 27 context.set('content-type', 'text/html'); 28 29 return html; 30};

We need those dependencies:

  • @react-email/components and react for building component

  • @babel/register and @babel/preset-react @react-email/render for transform and render,

Let's install it in the Dependencies panel, after all dependencies are installed, click the debug button to test your code. If there are no other errors, you will see your email template HTML in the console and response panels.

deploy.gif

You can also host the template online by clicking the Deploy button, copy the invoke URL to the browser, now you can check and review what your email looks like when you open it in your mailbox.

screenshot-email

Using Resend SDK to deliver the email

Now we have the data and email template, the last thing is to send the update notification to the subscriber through email.

Prerequisites

Let's learn how to send your first email using the Resend Node.js SDK. First, we need add a deliver.ts function as an email poster:

Before coding, To get the most out of this guide, you’ll need to:

We need the Resend Node.js SDK. Search resend lib and install this SDK in the Dependencies panel like before. The example code from docs is very easy to use.

js
1import { Resend } from 'resend'; 2// use your own key 3const resend = new Resend('re_123456789'); 4 5try { 6 const data = await resend.emails.send({ 7 from: 'Acme <onboarding@resend.dev>', 8 to: ['delivered@resend.dev'], 9 subject: 'Hello World', 10 // use your email template 11 html: '<strong>It works!</strong>', 12 }); 13 14 console.log(data); 15} catch (error) { 16 console.error(error); 17}

Send email using HTML template

Send an email by using the html parameter with the template you have done before, the to from the database you have collected. See the full deliver source code.

js
1require('@babel/register')({ 2 presets: ['@babel/preset-react'], 3}); 4 5type RecordItem = { 6 email: string, 7}; 8 9import aircode from 'aircode'; 10 11const getEmail = require('./email.jsx'); 12const { render } = require('@react-email/render'); 13 14const { Resend } = require('resend'); 15 16const resend = new Resend(process.env.RESEND_API_KEY); 17 18module.exports = async function (params: any, context: any) { 19 console.log('Received params:', params); 20 21 const { title, excerpt, coverImage, href } = params; 22 23 const html = render(getEmail(title, excerpt, coverImage, href)); 24 25 const emailTables = aircode.db.table('emails'); 26 27 const emailsRecords = await emailTables 28 .where() 29 .projection({ email: 1 }) 30 .find(); 31 32 console.log('emailsRecords', emailsRecords); 33 34 if (emailsRecords && emailsRecords.length) { 35 const emailList = emailsRecords.map((item) => item.email); 36 37 console.log('emails', emailList); 38 39 // Sending to a batch of recipients is not yet supported in Resend, but you can send to each recipient individually. 40 // you can try for of method to send your email 41 // See https://resend.com/docs/knowledge-base/can-i-send-newsletters-with-resend 42 try { 43 const data = await resend.emails.send({ 44 from: 'hello@aircode.io', 45 to: emailList, 46 subject: `AirCode updates: ${title}`, 47 html, 48 }); 49 50 console.log(data); 51 return { 52 data, 53 code: 0, 54 message: 'success', 55 }; 56 } catch (error) { 57 console.error(error); 58 return { 59 data: null, 60 code: 1, 61 message: error, 62 }; 63 } 64 } 65 return { 66 data: null, 67 message: 'There is no mailing list to deliver, please add email.', 68 }; 69};

You need to paste this key RESEND_API_KEY form Resend into the AirCode environment settings before you test it, just like the below:

env-key.png

Add your post data in params for debugging email delivery, click the Debug button to send the first email.

screenshot-params

The test data you can paste to Params panel, then click the Debug Button for delivering test email:

json
1{ 2 "href": "https://aircode.io/blog/why-create-aircode", 3 "title": "What we are building?", 4 "excerpt": "AirCode is Your Serverless Node.js Stack for API Development,zero-config, all in one place.AirCode is Your Serverless Node.js Stack for API Development, zero-config, all in one place", 5 "coverImage": "https://ph-files.imgix.net/b41dc780-1623-4c46-90b9-1a0d514c5730.jpeg?auto=compress&codec=mozjpeg&cs=strip&auto=format&fit=max&dpr=2" 6}

email.png

Congratulations on getting things to work! 🎉

Conclusion

So far, you've learned how to create a beautiful email with React, communicate between a Next.js and Node.js app, and send email notifications using Resend SDK.

The source code for this tutorial is available here:

Thank you for reading! Kevin is here, If you have any questions, feel free to contact me. I can’t wait to see what you will build!