Max Schmitt

March 7 2024

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

In a previous guide, I demonstrated how to easily add a contact form to a Next.js application with the pages router.

Today, we'll build the same contact form but use the Next.js app router for the API route.

Just like last time, we need:

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

And we'll 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

We'll take the ContactForm component from the other post. We just need to make sure to add 'use client' to the top to mark it as a client component.

components/ContactForm.tsx

'use client'
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

To implement the API route for sending the email using the Next.js app router, we'll once again rely on Mailgun for sending the email.

Once again, you'll need to add the following environment variables:

  • 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

Also, make sure to install the mailgun.js and form-data packages:

$ yarn add mailgun.js form-data

The actual route handler is very similar to the one we built for the pages router. The difference lies mostly in how we handle the request and response.

I've highlighted the main differences to the previous pages router-based implementation below:

app/api/contact/route.ts

import Mailgun from 'mailgun.js'
import FormData from 'form-data'
import { NextRequest, NextResponse } from 'next/server'
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',
})
// Export the POST handler
export async function POST(request: NextRequest) {
// Get the form data from the request body
const { name, email, message } = await request.json()
// 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
return NextResponse.json({ status: 'ok' })
}

That's it

It's quite straight-forward to build this using the app router. I do like the new folder structure of the app directory and the fact that you can directly export function POST () {} from a route file.

I hope this contact form solution comes in handy for you!