Adding a Contact Form to Your Next.js Site

Prince Shammah

Prince Shammah / July 11, 2023

––– views

I recently had to build a portfolio site for an agency but I didn't want to use WordPress for it. I've been spending quite a while in the Vercel Ecosystem and wanted to do this with Next.js. It proved to be kinda challenging though. So I'm going to walk you through it.

First, you need to create your new Next.js project.

npx create-next-app@latest next-contact-form
cd next-contact-form

I'm using the new App router. You can use the page router if you feel more comfortable with it.

Next. You're going to install Flowbite React, Axios, and, Nodemailer. We'll need all of these.

npm i flowbite flowbite-react axios nodemailer

Next, we create a function to send emails using Axios.

lib/email.ts.

import axios from 'axios';

export type Email = {
    email: string;
    subject: string;
    message: string;
}

export type ResponseMessage = {
    isSuccessful: boolean;
    isError?: boolean;
    message: string;
};

export const sendEmail = async (email: Email) => {
    return axios({
        method: 'post',
        url: '/api/send-mail',
        data: email,
    });
};

As you can see, I have an Email and ResponseMessage type to simplify things. We will need ResponseMessage later. After creating this we will now create the API route to handle sending emails with Nodemailer.

api/send-email.ts.

import nodemailer from 'nodemailer';
import type { NextApiRequest, NextApiResponse } from 'next';

export default function handler(req:NextApiRequest , res: NextApiResponse) {

    const message = {
        from: process.env.NEXT_MAIL_USER,
        to: req.body.email,
        subject: req.body.subject,
        text: req.body.message
    };

    // Create a nodemailer transporter using SMTP
    const transporter = nodemailer.createTransport({
        host: process.env.NEXT_MAIL_HOST,
        port: 465,
        secure: true,
        auth: {
            user: process.env.NEXT_MAIL_USER,
            pass: process.env.NEXT_MAIL_PASS,
        },
    });

    // const transporter = nodemailer.createTransport({
    //   service: 'gmail',
    //   auth: {
    //     user: 'user@gmail.com',
    //     pass: 'passowrd',
    //   },
    // });

    if (req.method === 'POST') {
        transporter.sendMail(message, (err: Error | null, info) => {
            if (err) {
                res.status(404).json({
                    error: `Connection refused for ${err.message}`,
                });
            } else {
                return res.status(200).json({
                    success: `Message delivered to ${info.accepted}`,
                });
            }
        });
    }
}

I commented out the code for the Gmail transporter in case you might want to use that. Although a lot of your stuff is going to end up in Spam and I haven't found a fix for that yet.

Done? Cool. Now we create the contact form component. variables. Create a new file components/Contact.tsx.

'use client';
import { useState } from 'react';
import { FormEvent } from 'react';
import { Alert, Button,TextInput, Label, Textarea } from 'flowbite-react';
import { Email, sendEmail } from '@/lib/send-email';
import { AxiosError } from 'axios';

const Contact = () => {
    const [subject, setSubject] = useState('');
    const [email, setEmail] = useState('');
    const [message, setMessage] = useState('');
    const [responseMessage, setResponseMessage] = useState<ResponseMessage>(
        { isSuccessful: false, message: '' });

    const isDev: boolean = process.env.NODE_ENV === 'development';

    const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
        e.preventDefault();

        const mail: Email = {
            email,
            subject,
            message
        };

        try {
            const req = await sendEmail(mail);
            if (req.status === 200) {
                setResponseMessage({
                    isSuccessful: true,
                    message: 'We will get back to you soon.'
                });
                setEmail('');
                setSubject('');
                setMessage('');
            }
        } catch (error: unknown) {
            setResponseMessage({
                isSuccessful: false,
                isError: true,
                message: (error as AxiosError).message
            });
        }
    };

    const SentAlert = ({ response }: { response: ResponseMessage }) => {

        return (
            <Alert
                color={response.isSuccessful ? 'success' : 'failure'}
                onDismiss={() => setResponseMessage({ isSuccessful: false, message: '' })}
            >
                <span>
                    <p>
                        <span className="font-medium">
                            {response.isSuccessful ? 'Thank you. ' : 'Error: '}
                        </span>
                        {response.isSuccessful ? response.message : isDev ? response.message : 'Oops something went wrong. Please try again'}
                    </p>
                </span>
            </Alert>
        );
    };

    return (
        <section id="contact" className='bg-white dark:bg-gray-900'>

            <div className='py-8 lg:py-16 px-4 mx-auto max-w-screen-md'>
                <div className='border-t border-gray-400 w-48 h-2 mx-auto'/>
                <h2 className='mb-4 text-4xl tracking-tight font-extrabold text-center text-gr++++y-900 dark:text-white'>Contact Us</h2>
                <p className='mb-8 lg:mb-16 font-semibold text-center text-black dark:text-white sm:text-xl'>Got a idea you want built? Need details about us? Let us know.</p>
                <form onSubmit={handleSubmit} className='space-y-8'>

                    <div>
                        <Label htmlFor='email' className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>Your email</Label>
                        <TextInput 
                            addon='@'
                            value={email}
                            onChange={(e) => setEmail(e.target.value)}
                            type='email' id='email' placeholder='name@mail.com' required/>
                    </div>
                    <div>
                        <Label htmlFor='email' className='block mb-2 text-sm font-medium text-gray-900 dark:text-gray-300'>Subject</Label>
                        <TextInput 
                            value={subject}
                            onChange={(e) => setSubject(e.target.value)}
                            type='text' id='subject' placeholder='Subject' required/>
                    </div>
                    <div className='sm:col-span-2'>
                        <Label htmlFor='message' className='block mb-2 text-sm font-medium text-gray-900 dark:text-white'>Your message</Label>
                        <Textarea id='message' rows={6} className='' placeholder='Leave a comment...'
                            value={message}
                            onChange={(e) => setMessage(e.target.value)}
                        ></Textarea>
                    </div>
                    <Button type='submit' className='py-3 px-5 text-sm font-medium text-center text-white rounded-lg  sm:w-fit hover:bg-primary-800 focus:ring-4 focus:outline-none focus:ring-primary-300  dark:hover:bg-primary-700 dark:focus:ring-primary-800'>Send message</Button>
                </form>

                {(responseMessage.isSuccessful || responseMessage.isError) && (<SentAlert
                    response={responseMessage}/>)}

            </div>
        </section>);
};

export default Contact;

In order to use FlowBite React, we will need to create a context for it.

context/FlowbiteContext.ts

'use client';
import { Flowbite } from 'flowbite-react';
import { FC, PropsWithChildren } from 'react';
import { flowbiteTheme as theme } from '@/app/theme';

const FlowbiteContext: FC<PropsWithChildren> = function ({ children }) {
    return <Flowbite theme={{ theme }}>{children}</Flowbite>;
};

export default FlowbiteContext;

Since I'm using the App router I have a layout and page file to work with. We will add our created context to the layout file here.

src/app/layout.tsx

import { ReactNode} from 'react';
import './globals.css';
import { Inter } from 'next/font/google';
import Script from 'next/script';
import type { Metadata } from 'next';

import FlowbiteContext from '@/context/FlowbiteContext';

const inter = Inter({ subsets: ['latin'] });

export const metadata: Metadata = {
    title: {
        default: 'Contact Form'
    },
    colorScheme: 'dark',
    description: 'Testing out a Contact form with Next.js',
};

export default function RootLayout({
    children,
}: {
  children: ReactNode
}) {
    return (
        <html lang="en" className="dark">
            <FlowbiteContext>
                <body
                    className={ inter.className }>{ children }</body>
            </FlowbiteContext>
            <Script
                src='https://cdnjs.cloudflare.com/ajax/libs/flowbite/1.7.0/flowbite.min.js' />
        </html>

    );
}

Now my src/app/page.tsx looks like this:

'use client';
import { Flowbite } from 'flowbite-react';
import Contact from '@/components/Contact';

const Home = () => {
    return (
        <Flowbite>
            <Contact/>
        </Flowbite>

    );
};

export default Home;

After doing all this we can test out our code using:

npm run dev

If all went according to plan, you should see this:

On sending an email successfully, this is what you will then see:

And that's how you add a contact form to your Next.js App.