Add a working contact form to your NextJS site

Having a contact form on your website allows your visitors to reach you for any feedback or best case for any business opportunity. Creating one via NextJS is
You have your contact section UI ready and now wondering how to use the data from the contact form and send an email to a valid email address. In this blog let's cover how we can create an API in NextJS 14 using app router that will receive contact form data and send an email with it. This guide can benefit you in two ways:
- Build upon the contact UI and API mentioned below for your website
- Use the contact API only for your existing UI
Prerequisites
- A NextJS 14 project up and running
- The project uses app router system
- Tailwind CSS for the contact form design
1. (Optional) Create a contact form
I am using Tailwind for the code example below. The basic assumption is that you have an contact form UI ready and the data is captured once the user click on send button. Below is a sample that can be used as a base to expand upon:
2. Create a new API route for contact form
Create a new api directory inside app and add a contact folder. Inside the contact folder add a file named route.js
:
This will create a route for your API like: /api/contact
. NextJS uses your folder structure to create URL path https://nextjs.org/docs/app/building-your-application/routing/route-handlers.
Let's now start building our contact API to receive JSON data from UI that we created above and send email using the very popular [Nodemailer] (https://www.npmjs.com/package/nodemailer).
I'll be using gmail to create this example with app specific password. You can use email service of your choice just find the right configurations like host, port etc.
// app/api/contact/route.js
import { createTransport } from 'nodemailer';
const transporter = createTransport({
host: 'smtp.gmail.com',
port: 465,
secure: true,
auth: {
user: sender_email_address,
pass: sender_email_password
}
});
export async function POST(req) {
const { name, email, message } = await req.json();
console.log(`You have an email from: ${name} - ${email} - ${message}`);
try {
const info = await transporter.sendMail({
from: `Sender_Name" <${sender_email_address}>`,
to: `MyAwesomeSite <reciever_email_address>`,
subject: `Contact form - MyAwesomeSite`,
html: `
<p>You have an email from:</p>
<p>${name}</p>
<p>${email}</p>
<p>${message}</p>
`
});
return Response.json(
{ status: 'ok', messageId: info.messageId || null },
{ status: 200 }
);
} catch (error) {
return Response.json({ status: 'error', messageId: null }, { status: 500 });
}
}
Your API is ready to test via Postman or VS Code Thunder client: http://localhost:3000/api/contact
3. Let's hook the contact form with the API that we created
- I have used the Fetch API to make a POST request to the API that we created above.
- Let's set the isLoading value to true when API call starts and on success/error we can show some msg on UI in form of a toast notification or just a alert.
- We'll also reset the state after 5 seconds contact section.
'use client';
import { useState } from 'react';
export default function Contact() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [message, setMessage] = useState('');
const [isLoading, setIsLoading] = useState(false);
const submitData = async () => {
setIsLoading(true);
let response = await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify({
name,
email,
message
}),
headers: {
'Content-type': 'application/json'
}
});
response = await response.json();
if (response.status === 'ok') {
// show success msg
} else {
// show failed msg
}
// after 5 seconds reset the UI
setTimeout(() => {
setName('');
setEmail('');
setMessage('');
setIsLoading(false);
}, 5000);
};
// basic validation to determine the disabled state of send button
const isDisabled =
isLoading || !name || !email || !message;
return (
<>
<div className="gap-6 p-4">
<input
type="text"
placeholder="Name"
className="input input-bordered mb-2"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
className="input input-bordered mb-2"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<textarea
placeholder="Your Message"
className="textarea textarea-bordered textarea-md mb-2"
value={message}
onChange={(e) => setMessage(e.target.value)}
></textarea>
</div>
<button
className={`btn btn-primary ${isDisabled ? 'btn-disabled' : ''}`}
disabled={isDisabled}
onClick={submitData}
role="button"
>
Send
</button>
<>
);
}
This is just a very basic implementation on the UI and the API for you to get started. Below will be some of the must have enhancements:
- form data validations on UI
- input param validation on the API
- better response code checks on the UI
- loader spinner when API is triggered for better UX