Geschrieben von Erik

Serverless Contact Form

Serverless contact form with Next.js Api Routes and Nodemailer

Click for German version


Introduction

I would like to show you in this blog article how you can quickly build a minimalistic and good looking contact form in Next.js, which sends requests with Axios to the backend, which in turn sends mails with Nodemailer. For the backend we don't use a middleware server like Express, but the Next.js Api Routes which were introduced with Next.js 9. So there are no tools needed for front- *and* backend except for Next.js, Nodemailer and Axios.

Throughout this article, I will comment on the code passages in comments of this form [1], which I will explain separately under each code.

This blog article will explain how to create a Next.js Api Route. But it won't include a basic Next.js tutorial, you are expected to know how to create a basic Next.js page. If this is not the case, I recommend the very good official tutorial: https://nextjs.org/learn/basics/getting-started

Add Nodemailer and Axios to your Next.js project first. For styling reasons you can also add react-autosize-textarea. This is of course not necessary for functionality:

yarn add nodemailer axios react-autosize-textarea


Frontend

Add the contact form to your index.js in the "pages" folder, which we will create next:

import Contact from "../components/pattern/contact"

const Index = () => {
  return (
  <div>
    <Contact/>
  </div>
  )
}

Depending on your folder structure, we now create the contact.js in the desired folder. So for me this is /components/pattern/contact.js

The contact.js looks like this:

import { Component } from "react"
import TextareaAutosize from "react-autosize-textarea"
import { sendContactMail } from "../../networking/mail-api" 
//[1]

class Contact extends Component {
    state = {
        formButtonDisabled: false,
        formButtonText: "Send",
        name: "",
        mail: "",
        formContent: ""
    }

    render() {    
        const { formButtonText, formButtonDisabled, name, mail, formContent } = this.state
        
        const btnClass = formButtonDisabled ? "disabled" : "" 
//[2]

        return (
            <div>               
                <div className="grid"> 
//[3]
                    <div className="col-8">
                        <h2>Contact form title</h2>
                        <p>Contact form introduction text</p>                    
                    </div>
                </div>
                <div className="grid">
                    <div className="col-4">
                        <input
                            type="text"
                            placeholder="Name"
                            value={name}
                            name="fname"
                            onChange={this.onNameChange} />
                    </div>
                    <div className="col-4">
                        <input
                            type="email"
                            placeholder="E-Mail"
                            value={mail}
                            name="email"
                            onChange={this.onMailChange} />
                    </div>
                </div>
                <div className="grid">
                    <div className="col-8">
                        <TextareaAutosize
                            name="text"
                            placeholder="Message"
                            value={formContent}
                            onChange={this.onFormContentChange}
                            style={{
                                minHeight: "48px",
                                width: "100%",
                                border: "none",
                                borderRadius: "0px",
                                margin: "8px 0px",
                                resize: "none",
                                padding: "0px",
                                paddingBottom: "14px",
                                WebkitAppearance: "none",
                                MozAppearance: "none"
                            }} /> 
//[4]

                    </div>
                    <div className="col-8">
                        <button
                            className={btnClass}
                            type="submit"
                            onClick={this.submitContactForm}
                            disabled={formButtonDisabled}>

                           {formButtonText}
                       </button>
                    </div>
                </div>
                <style jsx>{`
                    .grid {
                        display: flex;
                        flex-direction: row;
                        flex-wrap: wrap;
                        max-width: 1280px;
                        margin-right: auto;
                        margin-left: auto;
                        padding-left: 12px;
                        padding-right: 12px;
                    }

                    .col-4,
                    .col-8 {
                        padding: 8px 12px;
                        box-sizing: border-box;
                    }
                    .col-4 {
                        flex-basis: 50%;
                        max-width: 50%;
                    }
                    .col-8 {
                        flex-basis: 100%;
                        max-width: 100%;
                    }
                    @media only screen and (max-width: 768px) {
                        .grid {
                            flex-direction: column;
                            padding-left: 0px;
                            padding-right: 0px;
                        }
                        .col-4,
                        .col-8 {
                            padding-left: 24px;
                            padding-right: 24px;
                            flex-basis: 100%;
                            max-width: 100%;
                        }
                    }
                    input[type=text], input[type=email] {
                        height: 48px;
                        width: 100%;
                        border: none;
                        border-radius: 0px;
                        border-bottom: 1px solid #121212;
                        margin: 8px 0px;
                        box-shadow: none;
                        -webkit-appearance: none;
                        -moz-appearance: none;
                        padding: 0px;
                        outline: none;
                    }

                    ::placeholder {
                        color: #C8CBCE;
                    }

                    ::-ms-input-placeholder {
                        color: #C8CBCE;
                    }

                    button {
                        padding: 0px 24px;
                        height: 48px;
                        background-color: #F83850;
                        margin: 16px 0px;
                        border: none;
                        border-radius: 0px;
                        cursor: pointer;
                        color: #fff;
                    }

                    .disabled {
                        background-color: #fff;
                        color: #121212;
                        cursor: auto;
                        padding-left: 0px;
                    }
                `}</style>
            </div>
        )
    }

    onNameChange = (event) => {
        this.setState({ name: event.target.value })
    }
//[5]

    onMailChange = (event) => {
        this.setState({ mail: event.target.value })
    }

    onFormContentChange = (event) => {
        this.setState({ formContent: event.target.value })
    }

    submitContactForm = async (event) => {
        event.preventDefault()
//[6]

        const recipientMail = yourmail@example.com
        const { name, mail, formContent } = this.state

        const res = await sendContactMail(recipientMail, name, mail, formContent)
        if (res.status < 300) {
            this.setState({
                formButtonDisabled: true,
                formButtonText: "Thanks for your message",
                name: "",
                mail: "",
                formContent: ""
            })

        } else {
            this.setState({ formButtonText: "Please fill out all fields." })
        }
//[7]
    }
}

export default Contact

[1] Here we import the Axios function that we write next. It replaces the "send form"-function, which the browser would take over otherwise. This allows us to communicate with the backend without having to send the form to open a new page.

[2] With the states formButtonDisabled and formButtonText we change the "send button" below, if the form was sent successfully or not successfully.

[3] In the style jsx below we implement our own small Flexbox Grid.

[4] The component react-textarea-autosize is styled via the style property. It gets about the same styles that we choose down below for the other two form fields. There are so many because we have to deactivate the standard styles of the respective browsers and want to make them more beautiful.

[5] onNameChange and the other two functions are used to keep the entered value and the react state synchronous.

[6] event.preventDefault() will prevent executing the default function that the HTML standard has implemented to handle forms.

[7] This calls the Axios function that handles the form. Depending on whether this is successful or not, the button text is changed. sendContactMail() gets all relevant data here.


Backend

The first of three parts is done. Next we create the Axios function to communicate with the backend. This is located in components/networking/mail-api.js in my root folder:

import axios from "axios"

export const sendContactMail = async (recipientMail, name, senderMail, content) => {
    const data = {
        recipientMail,
        name,
        senderMail,
        content
    }

    try {
        const res = await axios({
            method: "post",
            url: "/api/contact",
            headers: {
                "Content-Type": "application/json"
            },
            data
        })
        return res

    } catch (error) {
        return error
    }
}
//[1]

[1] Axios executes a POST request to /api/contact without opening the URL, as is usually the case with POST requests. /api/contact is our backend in the form of a Next.js Api Route.

A Next.js Api Route can be created by creating an "api" folder in the "pages" folder. So for me it's like this: /pages/api/contact.js.Now the contact.js is available on your website under the following link: example.com/api/contact

No additional configuration is required for this to work.

For more information you can read the official documentation: https://github.com/zeit/next.js#api-routes

import nodemailer from "nodemailer"

const emailPass = "yourPassword"

const transporter = nodemailer.createTransport({
    host: "smtp.ionos.de",
    port: 25,
    auth: {
        user: "yourUser@example.com",
        pass: emailPass
    }
})
//[1]

export default async (req, res) => {
    const { senderMail, name, content, recipientMail } = req.body
//[2]

    // Check if fields are all filled
    if (senderMail === "" || name === "" || content === "" || recipientMail === "") {
        res.status(403).send("")
        return
    }
//[3]

    const mailerRes = await mailer({ senderMail, name, text: content, recipientMail })
    res.send(mailerRes)
//[4]
}

const mailer = ({ senderMail, name, text, recipientMail }) => {
    const from = name && senderMail ? `${name} <${senderMail}>` : `${name || senderMail}`
    const message = {
        from,
        to: `${recipientMail}`,
        subject: `New message from ${from}`,
        text,
        replyTo: from
    }
//[5]

    return new Promise((resolve, reject) => {
        transporter.sendMail(message, (error, info) =>
            error ? reject(error) : resolve(info)
        )
    })
//[6]
}

[1] Here we create the Nodemailer "Transporter". For this you need an e-mail account with password access, whose SMTP server you can use. We use an account of 1&1 (Ionos).

With Gmail this is more complicated. For this see the Nodemailer documentation.

[2] Here Next.js pulls the da from the req.body (Request Body) that we sent with Axios in the "data" object and creates variables from it.

[3] If a field is empty, a status code 403 is returned. This triggers in the frontend that the button shows that you have to fill in all fields.

[4] Here the mailer function is called from below, which in turn calls sendMail() of the nodemail transporter. When this is done, it sends a status code which is sent as res (response) and determines whether a success or failure message is displayed in the frontend, depending on what is true.

[5] Here the e-mail form with sender address, recipient address, subject, content and reply address is generated.

[6] Nodemailer makes sure that the mail is sent via the SMTP server you specified above as the transporter.

— erk

Beginnen wir Neues

Kontakt aufnehmen