Max Schmitt

March 1 2024

Next.js: How to add a Contact Form (Pages Router)

In this guide, I'll show how you can easily add a contact form to a Next.js application when using the pages router.

I've also published a guide for adding a contact form to a Next.js app using the app router.

Here's what we'll build:

  • A simple <ContactForm /> component
  • An API route to send the form data to an email address of our choice

That's all we need to end up with something like this:

If you'd like to jump straight into the code, feel free to check out the repository on GitHub.

Let's get started!

1. Create a ContactForm component

The <ContactForm /> component needs to take a name, email and message as input, and send this data to an API route when the form is submitted.

We won't be using any fancy form libraries for this example. We'll rely on the native form elements and their built-in validation instead.

components/ContactForm.tsx

import React, { useState } from 'react'
function ContactForm() {
const [loading, setLoading] = useState(false)
const [successMessage, setSuccessMessage] = useState('')
const onSubmit = async (e: React.FormEvent) => {
// Prevent the form from submitting the traditional way
e.preventDefault()
// Don't submit twice
if (loading) {
return
}
// 👇 A nice little track to get all the form values as an object
const form = e.target as HTMLFormElement
const formValues = Object.fromEntries(new FormData(form).entries())
setLoading(true)
setSuccessMessage('')
try {
await fetch('/api/contact', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formValues),
}).then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`)
}
return response.json()
})
setLoading(false)
setSuccessMessage('Thank you for contacting us!')
// Reset the form values after a successful submission
form.reset()
} catch (err) {
console.error(err)
alert('An error occurred while sending your message...')
setLoading(false)
}
}
return (
<form onSubmit={onSubmit}>
<label>
<span>Name</span>
<input type="text" name="name" required />
</label>
<label>
<span>Email</span>
<input type="email" name="email" required />
</label>
<label>
<span>Message</span>
<textarea name="message" required />
</label>
<button disabled={loading} type="submit">
Send message!
</button>
{successMessage && <p>{successMessage}</p>}
</form>
)
}
export default ContactForm

2. Send an email from a Next.js API route

Now that we have our <ContactForm /> component built, let's implement the API route that will accept the form data and send an email to us.

There are many transactional email services but I like using Mailgun because they have a generous free tier.

After you've got that setup, you'll need to get the following environment variables and save them to your .env file:

  • CONTACT_FORM_FROM_EMAIL: The email address that the contact form submissions will be sent from
  • CONTACT_FORM_TO_EMAIL: The email address that the contact form submissions will be sent to
  • MAILGUN_DOMAIN: The domain that you've set up in Mailgun
  • MAILGUN_API_KEY: The API key that you've set up in Mailgun

We'll also need two packages: mailgun.js and form-data. You can install them by running:

$ yarn add mailgun.js form-data

Then go ahead and add the following API route to your Next.js application:

pages/api/contact.ts

import type { NextApiRequest, NextApiResponse } from 'next'
import Mailgun from 'mailgun.js'
import FormData from 'form-data'
const CONTACT_FORM_FROM_EMAIL = process.env.CONTACT_FORM_FROM_EMAIL as string
const CONTACT_FORM_TO_EMAIL = process.env.CONTACT_FORM_TO_EMAIL as string
const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN as string
const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY as string
const mailgun = new Mailgun(FormData)
const mg = mailgun.client({
username: 'api',
key: MAILGUN_API_KEY,
// This needs to be customized based on your Mailgun region
url: 'https://api.eu.mailgun.net',
})
async function contact(req: NextApiRequest, res: NextApiResponse<string | void>) {
// Only allow post requests
if (req.method !== 'POST') {
return res.status(404).end()
}
// Get the form data from the request body
const { name, email, message } = req.body
// Put together the email text
const text = ['From: ' + name + '<' + email + '>\n', message].join('\n')
// Send the email using Mailgun
await mg.messages.create(MAILGUN_DOMAIN, {
subject: 'New contact form submission',
from: CONTACT_FORM_FROM_EMAIL,
to: CONTACT_FORM_TO_EMAIL,
text,
'h:Reply-To': email,
})
// Send a 200 OK response
res.status(200).json({ status: 'ok' } as any)
}
export default contact

That's it

That was all that we need. I really like that Next.js makes it so easy to sprinkle in some server-side code for things like this.

I know they're pushing to do more and more backend stuff with Next.js with server actions but I haven't been able to get convinced to use it yet for full-blown APIs. But for simple things like contact form submissions, Next.js API routes are perfect.