By Sahil Khan
Contact forms are essential for portfolios, SaaS platforms, and production-grade websites, enabling seamless communication between clients and service providers. There are many ways to implement them, but in this blog, we’ll explore how to receive emails from website visitors using AWS Lambda, Resend API, and React.js.
We’ll build a simple Contact Us form using React.js and Tailwind CSS, validate the input, and send the data via an HTTP request to an AWS Lambda function. This function will revalidate and format the data before sending it as an email using Resend.
💡 If you don’t have an AWS account? You can use Vercel’s serverless functions for free with hobby projects. If you're interested in learning about Vercel functions, stay tuned—I'll write a guide on that soon.
🛠️ Setting Up the Serverless Framework
- Install the Serverless Framework globally (if you haven't already):
npm i serverless -g
- Create a new project:
serverless
Note: The Serverless Framework needs access to your AWS credentials. Running the command above will prompt you to configure your credentials if not already set.
Follow the official credential setup guide if needed.
- Choose a template:
Select:
AWS / Node.js / HTTP API
Name your service and proceed.
Choose or create a new app:
❯ Create A New App
ecommerce
blog
acmeinc
Skip Adding An App
Select Create A New App, and name it anything you like.
💻 Time to Dive Into Code
🔧 Tech Stack
- React.js — for the contact form frontend
- Tailwind CSS — for styling
- Serverless — to manage AWS Lambda functions
- Resend — to send emails
- Zod — for schema validation
👤 What Are We Collecting?
The form will collect the following from users:
- Name
- Subject
- Message (query)
📦 Let’s Start with the Lambda Function
We'll begin with the backend. Your handler.js
file (from the Serverless template) includes a default function like this:
exports.hello = async (event) => {
return {
statusCode: 200,
body: JSON.stringify({
message: "This is a serverless function",
}),
};
};
Let’s clean that up and prepare it to process incoming data.
🧹 Clean the boilerplate:
exports.hello = async (event) => {};
📥 Parse incoming request data:
exports.hello = async (event) => {
const body = JSON.parse(event.body);
};
✅ Validating Input Using Zod
1. Install Zod:
npm i zod
2. Import it:
const { z } = require("zod");
exports.hello = async (event) => {
const body = JSON.parse(event.body);
};
3. Define the validation function:
const { z } = require("zod");
exports.hello = async (event) => {
const body = JSON.parse(event.body);
};
function validateBody(body) {
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
subject: z.string().min(10),
message: z.string().min(20),
});
return schema.safeParse(body);
}
4. Use it inside your handler:
const { z } = require("zod");
exports.hello = async (event) => {
const body = JSON.parse(event.body);
const result = validateBody(body);
if (!result.success) {
return {
statusCode: 400,
body: JSON.stringify({
message: result.error.issues,
}),
};
}
// Continue to sending email...
};
// validate function
📬 Send the Email with Resend
1. Import Resend:
const { Resend } = require("resend");
2. Initialize Resend client:
const resend = new Resend(process.env.RESEND_API_KEY);
3. Send the email:
const { data, error } = await resend.emails.send({
from: "YourName <username@yourdomain.com>",
to: ["youremail@example.com"],
subject: "New Contact Message from Your Portfolio",
html: `
<p><strong>From:</strong> ${body.name}</p>
<p><strong>Email:</strong> ${body.email}</p>
<p><strong>Subject:</strong> ${body.subject}</p>
<p><strong>Message:</strong> ${body.message}</p>
`,
});
4. Handle errors:
if (error) {
return {
statusCode: 500,
body: JSON.stringify({ error }),
};
}
5. Final success response:
return {
statusCode: 200,
body: JSON.stringify({
message: "Email sent successfully",
success: true,
}),
};
The Final code will look like
const { Resend } = require("resend");
const { z } = require("zod");
const resend = new Resend(process.env.RESEND_API_KEY);
exports.hello = async (event) => {
const body = JSON.parse(event.body);
const result = validateBody(body);
if (!result.success) {
return {
statusCode: 400,
body: JSON.stringify({
message: result.error.issues,
}),
};
}
const { data, error } = await resend.emails.send({
from: "YourName <username@yourdomain.com>",
to: ["youremail@example.com"],
subject: "New Contact Message from Your Portfolio",
html: `
<p><strong>From:</strong> ${body.name}</p>
<p><strong>Email:</strong> ${body.email}</p>
<p><strong>Subject:</strong> ${body.subject}</p>
<p><strong>Message:</strong> ${body.message}</p>
`,
});
if (error) {
return {
error: JSON.stringify(error),
statusCode: 500,
};
}
return {
statusCode: 200,
body: JSON.stringify({
message: "Email sent successfully",
success: true,
}),
};
};
function validateBody(body) {
const schema = z.object({
name: z.string().min(2),
email: z.string().email(),
subject: z.string().min(10),
message: z.string().min(20),
});
return schema.safeParse(body);
}
🔧 Configuring serverless.yml
for Deployment
Now let’s configure the serverless.yml
file so your AWS Lambda function can deploy correctly and securely.
org: yourorganization # (optional) your Serverless Dashboard organization
app: somexyz # (optional) your app name
service: somexyz # name of your service/project
provider:
name: aws
runtime: nodejs20.x
region: ap-south-1 # choose your preferred AWS region
httpApi:
cors: true # enables CORS for cross-origin requests
functions:
hello:
handler: handler.hello
environment:
RESEND_API_KEY: ${env:RESEND_API_KEY} # reads the key from your .env file
events:
- httpApi:
path: /send/mail
method: post
💡 Make sure the
path
andmethod
match what your frontend will use. APOST
method is necessary for sending a body with the request.
🔐 Create a .env
File
In the root of your project (same level as serverless.yml
), create a .env
file and add your Resend API key like so:
RESEND_API_KEY="your-resend-api-key"
🚀 Deploy Your Serverless Function
Open your terminal, make sure you're in the root directory (where your serverless.yml
is), and run:
sls deploy
The Serverless Framework will:
- Package your function
- Upload it to AWS Lambda
- Set up the HTTP endpoint
- Return a live API URL like:
https://xxxxx.execute-api.ap-south-1.amazonaws.com/send/mail
You’ll use this URL to connect your React frontend to your backend.
💬 Creating the Contact Form (Client-Side)
Now let’s build the React frontend to let users send messages through your serverless email API.
Here’s the structure of the form:
- Name
- Subject
- Message
All fields are mandatory. We’ll use the useState
hook to capture and manage the form data.
Logic & Submission
We’ve implemented a handleSubmit
function that performs:
- Validation of fields
- API call to the AWS Lambda endpoint
- Loading indicator and success feedback
Make sure to store your deployed API URL in a .env
file at the root of your project like so:
VITE_MAIL_ENDPOINT=https://your-aws-lambda-url/send/mail
Complete JSX Contact Form with Tailwind Styling
import { useState } from "react";
const Contact = () => {
const [formData, setFormData] = useState({
name: "",
email: "",
subject: "",
message: "",
});
const [isMailSuccess, setMailSuccess] = useState(false);
const [isMailSending, setMailSending] = useState(false);
const validateForm = () => {
const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/;
if (
!formData.name ||
!formData.email ||
!formData.subject ||
!formData.message ||
!emailRegex.test(formData.email)
) {
return {
message: "All fields are required and email must be valid.",
status: false,
};
}
return {
message: "",
status: true,
};
};
const handleSubmit = async (e) => {
e.preventDefault();
setMailSending(true);
const { status, message } = validateForm();
if (!status) {
setMailSending(false);
alert(message);
return;
}
try {
const response = await fetch(import.meta.env.VITE_MAIL_ENDPOINT, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (data.success) {
setMailSuccess(true);
setFormData({
name: "",
email: "",
subject: "",
message: "",
});
setMailSending(false);
setTimeout(() => {
setMailSuccess(false);
}, 1000);
}
} catch (error) {
setMailSending(false);
setMailSuccess(false);
alert("Something went wrong.");
}
};
return (
<div className="fixed inset-0 flex items-center justify-center bg-black bg-opacity-60 z-50">
<div className="bg-zinc-900 rounded-xl p-6 w-80 md:w-full max-w-lg shadow-lg relative">
<h2 className="text-2xl font-bold mb-4 text-white text-center">
Get in Touch
</h2>
<form onSubmit={handleSubmit} className="space-y-4 text-white">
<div>
<label className="block text-sm font-medium text-white">
Name
</label>
<input
type="text"
placeholder="Your name"
value={formData.name}
onChange={(e) =>
setFormData({ ...formData, name: e.target.value })
}
className="mt-1 w-full border border-gray-300 rounded-lg px-4 py-2 placeholder-zinc-400 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-white">
Email
</label>
<input
type="email"
placeholder="you@example.com"
value={formData.email}
onChange={(e) =>
setFormData({ ...formData, email: e.target.value })
}
className="mt-1 w-full border border-gray-300 rounded-lg px-4 py-2 placeholder-zinc-400 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-white">
Subject (10 words min)
</label>
<input
type="text"
placeholder="Subject"
value={formData.subject}
onChange={(e) =>
setFormData({ ...formData, subject: e.target.value })
}
className="mt-1 w-full border border-gray-300 rounded-lg px-4 py-2 placeholder-zinc-400 focus:outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-white">
Message (20 words min)
</label>
<textarea
placeholder="Your message..."
value={formData.message}
onChange={(e) =>
setFormData({ ...formData, message: e.target.value })
}
className="mt-1 w-full border border-gray-300 rounded-lg px-4 py-2 h-28 resize-none placeholder-zinc-400 focus:outline-none"
></textarea>
</div>
{isMailSending ? (
<div className="flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-gray-200"></div>
</div>
) : (
<button
type="submit"
className="w-full py-2 px-4 rounded-lg bg-cyan-500 hover:bg-cyan-600 transition text-white"
>
{isMailSuccess ? "Sent successfully" : "Send"}
</button>
)}
</form>
</div>
</div>
);
};
export default Contact;
In the above pic you can see how the form will look like but in you case it might not look like this because I have some parent css that are not mentioned here, so feel free to add some different style.
Test time
Now any one can send mail to you from you page, lets take a look how it look like
After writing the inputs when user will click on send It will start a loader and as the message sent sucessfully it will show a message of _Sent successfully _ and you will receive mail in your inbox.
There is a high probability that your mail move to Spam folder, if then you need to
Report not spam
so that you can get notification.
In the above Image you can see the mail I receive in my Inbox
Live Demo
Visit my Portfolio for live demo.
Conclusion
Building a contact form that sends real-time emails using React.js, AWS Lambda, and Resend may sound intimidating at first, but as we've seen, it’s surprisingly smooth when broken down step by step. With Zod ensuring your data is valid, Serverless Framework handling your AWS deployment, and Resend taking care of email delivery, you now have a powerful, scalable solution that’s perfect for portfolios, SaaS products, and production-ready sites.
If you like this blog, try to implement it in you own website so that you can reach out to your end-users.
If you'd like to give me any feedback, reach out to me at my X handle or visit my Website and if you want to use Vercel Functions
insted of AWS lambda just leave a comment below I'll surely give a guide on Vercel Functions
.
:-)