Introduction
Sending emails in a contact form looks simple. You receive a request, call Nodemailer, and send the message. But in production, this approach quickly breaks down.
Email handling should be treated as a pipeline with validation, processing, and delivery steps. This ensures reliability and scalability as usage grows.
The Problem
A common implementation sends emails directly inside the API request.
await transporter.sendMail(mailOptions);
- Slow responses due to blocking email sending
- Failures directly impact user requests
- No retry mechanism for failed deliveries
- No protection against spam or abuse
This works in development but fails under real usage.
System Design / Approach
A better approach is to design an email pipeline with clear stages.
- Validate input data
- Store or queue the request
- Process email sending asynchronously
- Handle failures and retries
This decouples user requests from email delivery.
Implementation
Step 1: Setup Nodemailer
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
service: "gmail",
auth: {
user: process.env.EMAIL,
pass: process.env.PASSWORD
}
});
This configures the email transport.
Step 2: Validate Input
if (!email || !message) {
throw new Error("Invalid input");
}
Validation prevents bad data and spam.
Step 3: Send Email
await transporter.sendMail({
from: email,
to: "your@email.com",
subject: "Contact Form",
text: message
});
Basic email sending using Nodemailer.
Step 4: Decouple with Queue
Instead of sending emails directly, push them to a queue.
await queue.add("send-email", { email, message });
This improves reliability and scalability.
Trade-offs
| Approach | Benefit | Cost |
|---|---|---|
| Direct sending | Simple | Blocking and unreliable |
| Queued pipeline | Reliable and scalable | More infrastructure |
Real-World Impact
- Faster API response times
- Improved email delivery success
- Better protection against spam
- More reliable communication system