Serverless Kontaktformular mit Next.js Api Routes und Nodemailer

Click for English version


Einleitung

Ich möchte euch in diesem Blog-Artikel vorstellen wie ihr schnell ein minimalistisches und gut aussehndes Kontaktformular in Next.js baut, was mit Axios Requests an das Backend schickt, welches wiederum Mails mit Nodemailer verschickt. Für das Backend benutzen wir keinen Middleware Server wie Express, sondern die Next.js Api Routes , die mit Next.js 9 eingeführt worden. Es werden also für Front- und Backend keine Tools außer Next.js, Nodemailer und Axios benötigt.

Im gesamten Artikel werde ich in den Code Passagen in Kommentaren Anmerkungen dieser Form [1] machen, die ich unter dem jeweiligen Code gesondert erkläre.

Dieser Blog-Artikel wird erklären wie ihr eine Next.js Api Route anlegt. Es wird aber kein grundlegendes Next.js Tutorial beinhalten, es wird erwartet, dass ihr wisst, wie ihr eine grundlegende Next.js Seite erstellt. Sollte das nicht der Fall sein, empfehle ich euch das sehr gute, offiziele Tutorial: https://nextjs.org/learn/basics/getting-started

Fügt zu eurem Next.js Projekt zuerst Nodemailer und Axios hinzu. Aus Styling-Gründen könnt ihr außerdem react-autosize-textarea hinzufügen. Das ist für die Funktionalität aber natürlich nicht notwendig:

yarn add nodemailer axios react-autosize-textarea


Frontend

Fügt eurer index.js im "pages"-Ordner das Kontaktformular hinzu, was wir als nächstes erstellen:

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

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

Je nach eurer Ordnerstruktur legen wir jetzt im gewünschten Ordner die contact.js an. Bei mir ist das dann also /components/pattern/contact.js

Die contact.js sieht folgendermaßen aus:

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] Hier importieren wir die Axios Funktion, die wir als nächstes schreiben. Sie ersetzt das "Form abschicken", das sonst der Browser übernimmt. Dadurch können wir mit dem Backend kommunizieren, ohne dass sich durch das Form abschicken eine neue Seite öffnet.

[2] Durch die States formButtonDisabled und formButtonText verändern wir weiter unten den Senden-Button, wenn die Form erfolgreich bzw. nicht erfolgreich abgeschickt wurde.

[3] In der style jsx weiter unten implementieren wir unser eigenes kleines Flexbox Grid.

[4] Die Komponente react-textarea-autosize wird über die Style-Eigengeschaft gestylt. Sie bekommt ca. die selben Styles, die wir weiter unten für die anderen beiden Formularfelder wählen. Es sind so viele, um die standardmäßigen Stylings der jeweiligen Browser zu deaktivieren und schöner zu machen.


Ihr könnt die Komponente durch das standardmäßige <textarea> Element ersetzen.

[5] onNameChange und die anderen beiden Funktionen werden benutzt, um den eingegebenen Wert und den React State synchron zu halten.

[6] Mit event.preventDefault() verhindern wird, dass die standardmäßige Funktion ausgeführt wird, die der HTML-Standard implementiert hat, um mit Forms umzugehen.

[7] Hier wird die Axios-Funktion aufgerufen, die die Form handlet. Je nachdem, ob das erfolgreich oder nicht ist, wird der Button-Text geändert. sendContactMail() bekommt hier alle relevanten Daten mit.


Backend

Damit ist der 1. von 3 Teilen geschafft. Als nächstes legen wir die Axios-Funktion zur Kommunikation mit dem Backend an. Diese liegt bei mir vom root-Ordner ausgesehen in components/networking/mail-api.js:

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 führt einen POST-Request zu /api/contact aus, ohne die URL zu öffnen, wie es bei POST-Request normalerweise ist. /api/contact ist unser Backend in Form einer Next.js Api Route.


Als letztes legen wir die Next.js Api Route an.

Eine Next.js Api Route legt man grundlegend an, indem man im "pages"-Ordner einen "api"-Ordner anlegt. Bei mir ist es dann also folgendermaßen: /pages/api/contact.js.

Damit ist die contact.js jetzt auf eurer Website unter folgendem Link aufrufbar: example.com/api/contact

Damit dies funktioniert ist keine zusätzliche Konfiguration nötig.
Für nähere Infos könnt ihr euch die offiziele Dokumentation dazu durchlesen: https://github.com/zeit/next.js#api-routes

Hier ist die contact.js:

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] Hier erstellen wir den Nodemailer "Transporter". Dafür benötigt ihr einen E-Mail Account mit Passwortzugang, dessen Smtp Server ihr benutzen könnt. Wir benutzen den eines Accounts von 1&1 (Ionos).

Mit Gmail ist das komplizierter. Dazu auch die Nodemailer Dokumentation.

[2] Hier zieht Next.js aus dem req.body (Request Body), die Daten, die wir mit Axios im "data"-Objekt verschickt haben und erstellt daraus Variablen.

[3] Wenn ein Feld leer ist, wird ein Status-Code 403 zurückgeschickt. Das löst im Frontend aus, dass der Button anzeigt, dass man alle Felder ausfüllen müssen.

[4] Hier wird die Mailer-Funktion von weiter unten aufgerufen, die wiederum sendMail() des Nodemail-Transporters aufruft. Wenn dieser fertig ist, sendet er einen Status-Code der wiederrum als res (Response) gesendet wird und bestimmt, ob im Frontend eine Erfolgs- oder Misserfolgsnachricht anzeigt, je nachdem was zutrifft.

[5] Hier wird die E-Mail Form mit Absender-, Empfängeradresse, Betreff, Inhalt und Antwortadresse generiert.

[6] Nodemailer kümmert sich darum, dass die Mail über den SMTP-Server gesendet wird, den ihr oben als Transporter angegeben habt.

Damit sind wir fertig. Mit dieser Kombination könnt ihr in Next.js ein Kontaktformular erstellen, was einfach Mails verschickt.

— erk

Get in touch