- Published on
DIY Contact Form: Next.js, Nodemailer, and Protonmail

There are lots of shitty anti-solutions
Contact forms are a recurring point of unnecessary complexity in otherwise simple websites. The market is full of profoundly overengineered paid services which claim that they've solved the problem by introducing frameworks, dependencies, and workflows far heavier than the problem warrants. Recently found myself in this exact situation. Just needed a simple contact form for a medium-traffic website, and the usual suspects‒Sendgrid, Mailgun, Mailchimp‒ all felt like overkill. Not to mention, I wasn't thrilled about the idea of sharing my users' data with yet another third-party service, esp. with these jokers who can't even come up with a good name for their junk.
When I was looking at one of these "service providers" horrible ui, I had to supress my gag-reflex, and that's when it hit me: why am I not just using SMTP on Protonmail? It can be set up secure, and very privacy-focused, and I already had an account. The biggest upside, it would give me full control over the users' data. So, I cracked a cold one and opened a new terminal tab.
The Stack & Set-up
- Next.js, provides the a good React framework
- nodemailer for sending emails with, uh... node.
- Protonmail's SMTP server for our email service
First things first, I set up environment variables by creating a .env.local file in the project root:
// .env.local
PROTON_SMTP_USER=your.email@protonmail.com
PROTON_SMTP_TOKEN=your-app-specific-password
PROTON_SMTP_SERVER=smtp.protonmail.ch
PROTON_SMTP_PORT=587
I also add these to the CI/CD for deployment into the live environment.
nodemailer
Then I create an API route. In pages/api/submit-form.js:
import nodemailer from 'nodemailer'
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' })
}
const transporter = nodemailer.createTransport({
host: process.env.PROTON_SMTP_SERVER,
port: process.env.PROTON_SMTP_PORT,
secure: false, // It actually uses STARTTLS, there are no shared keys
auth: {
user: process.env.PROTON_SMTP_USER,
pass: process.env.PROTON_SMTP_TOKEN,
},
tls: {
ciphers: 'SSLv3',
rejectUnauthorized: true,
},
})
const { name, email, message } = req.body
try {
await transporter.sendMail({
from: process.env.PROTON_SMTP_USER,
to: 'your.destination@email.com',
subject: 'New Contact Form Submission',
text: `Name: ${name}\nEmail: ${email}\nMessage: ${message}`,
})
res.status(200).json({ message: 'Email sent successfully' })
} catch (error) {
console.error('Error sending email:', error)
res.status(500).json({ message: 'Error sending email' })
}
}
The Front-End: A basic-ass React Form
This is just a basic example to work off of:
import { useState } from 'react'
export default function ContactForm() {
const [formData, setFormData] = useState({ name: '', email: '', message: '' })
const handleSubmit = async (e) => {
e.preventDefault()
try {
const response = await fetch('/api/submit-form', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData),
})
if (response.ok) {
alert('Message sent successfully!')
setFormData({ name: '', email: '', message: '' })
} else {
throw new Error('Failed to send message')
}
} catch (error) {
alert('Error sending message. Please try again.')
}
}
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value })
}
return (
<form onSubmit={handleSubmit}>
<input
type="text"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Your Name"
required
/>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Your Email"
required
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Your Message"
required
/>
<button type="submit">Send</button>
</form>
)
}
Simple is good, and it works
Yup, a working no bullshit contact form, done in less time and with less cost than it takes to crack a cold one. Simple, efficient set-up that doesn't rely on any scam-ass third-party email services. It's perfect for low to mid-volume websites, and it offers complete control over users' data. Of course, this solution might not be suitable for high-traffic sites or market bros who need advanced email tracking features. But that ain't me, so the shoe fits just right. Don't forget to add some style your form. Also, you probs wanna add proper form validations and error handling. But you knew that, right?