DEV Community

Cover image for Build and Deploy a Serverless ReactJS Contact Form with Nodemailer and Netlify Functions
Isham Jassat
Isham Jassat

Posted on

Build and Deploy a Serverless ReactJS Contact Form with Nodemailer and Netlify Functions

ReactJS is a client side JavaScript framework. As such, while you can build good-looking contact forms with loads of client-facing functionality, you need to look elsewhere to do something that requires backend functionality such as send an email of add an entry to a database. This is the challenge I will be addressing in this post - how do you build and deploy a ReactJS contact form that will send an email when submitted.

Our toolbox will consist of:

  • ReactJS (obviously)
  • Axios (to post data)
  • Nodemailer (a Node.js package used to send emails via SMTP)
  • Netlify (for deploying)

We are going to capture data from our frontend form and post it to a backend url. We will build a Netlify function which will serve as our backend, take the form data we post and use Nodemailer to email the data to the recipient.

It really is as easy as it sounds.

Alt Text

Let's get started...

Front End

First we'll build the front end using ReactJS. To set things up we'll run npx create-react-app contact-form in our terminal window. This will provide the standard ReactJS app which we'll modify. And then we wait...

...once our react app is installed we run npm start to run the app in the browser. We open src/App.js and remove everything between the <header> tags so our file looks like this:

import React from 'react';
import logo from './logo.svg';
import './App.css';

function App() {
  return (
    <div className="App">
    </div>
  );
}

export default App;

While you're at it, get rid of import logo from './logo.svg'. Ahh, now we have a blank canvas 😌.

Now, create a new file in the src directory. This will be our contact form module. I'll call mine contact-form.js, you can call yours whatever you want. The basic structure of a React module is:

import React from 'react'

export default function FunctionName() {
    return (
        ...
    )
}

So we can start by building the structure of our contact form. I'm using material-us but again, you can use the CSS framework of you choice. All that matters is that you have a form:

import React from 'react'
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button"
import FormControl from "@material-ui/core/FormControl"

export default function Form() {

    return (
        <>

            <FormControl fullWidth={true}>
                <TextField required label="Full name" variant="filled" id="full-name" name="name" className="form-field" />
            </FormControl>
            <FormControl fullWidth={true}>
                <TextField required label="Email" id="email" name="email" variant="filled" className="form-field" onChange />
            </FormControl>
            <FormControl fullWidth={true}>
                <TextField required label="Message" variant="filled" name="message" multiline={true} rows="10" />
            </FormControl>
            <FormControl>
                <div style={{padding: 20}}>
                    <Grid container spacing={2}>
                            <div className="form-submit">
                                <Button variant="contained" color="primary">Submit</Button>
                            </div>
                        </Grid>
                    </Grid>
                </div>
            </FormControl>
    )
}

Now we can import the contact form in App.js. We modify App.js as follows:

import React from 'react';
import logo from './logo.svg';
import Form from './contactform'
import './App.css';

function App() {
  return (
    <div className="App">
      <Form />
    </div>
  );
}

export default App;

Capture Form Data

There are a few additions we need to make. First, we need to capture the form data. And what better way to do this than by using react hooks - specifically useState which we will use to track and update the 'state' of our data in realtime. Modify the first line in contactform.js to include the useState hook:

import React, { useState } from 'react'

Next, we instantiate useState variable. The variable is a two-item array with the first item being the state we are tracking, and the second item a function used to update that state:

export default function Form() {
    const [data, setData] = useState()

    return (
        ...
    )
}

Because we need to capture more than one field from the form, we'll setup data as an object:

export default function Form() {
    const [data, setData] = useState({name: '', email: '', message: '', sent: false, buttonText: 'Submit', err: ''})

    return (
        ...
    )
}

As you can see, we do this by simply setting the initial value of useState in object notation. We also setup a few utility items to track the status of our request and provide feedback to the user, namely sent, buttonText and err. More on these later.

Now we need a way to update our data object. Easy peasy - we setup a function that tracks changes to our form fields:

...

const [data, setData] = useState({name: '', email: '', message: '', sent: false, buttonText: 'Submit', err: ''})

const handleChange = (e) => {
    const {name, value} = e.target
        setData({
            ...data,
            [name]: value
    })
}

...

As its name suggests this function will be called whenever a user changes one of the form fields (i.e. by filling it in). The function uses object destructing to grab the name and value attributes of the form field being changed and updates the corresponding value in the data object.

The last thing we need to do is update the onChange and value attributes of our form fields to call this function as the user types:

<FormControl fullWidth={true}>
    <TextField required label="Full name" variant="filled" id="full-name" name="name" className="form-field" value={data.name} onChange={handleChange} />
</FormControl>
<FormControl fullWidth={true}>
     <TextField required label="Email" id="email" name="email" variant="filled" className="form-field" value={data.email} onChange={handleChange} />
</FormControl>
<FormControl fullWidth={true}>
     <TextField required label="Message" variant="filled" name="message" multiline={true} rows="10" value={data.message} onChange={handleChange} />
</FormControl>
<FormControl>
    <div className="form-submit">
        <Button variant="contained" color="primary">Submit</Button>
    </div>
</FormControl>

Handle Form Submissions

We need to setup a function that handles form submissions and we'll call it

const formSubmit = (e) => {
    e.preventDefault()
} 

We use the preventDefault function to stop the form redirecting the user to the backend URL which is its default behaviour.

Remember way back when, when I said we need to post the data to our backend URL? Well that's where Axios comes in - it's a promise-based http client and will serve our needs perfectly. Grab it by running npm i axios and once it's installed we can finish our submit function:

const formSubmit = (e) => {
        e.preventDefault();

        setData({
            ...data,
            buttonText: 'Sending...'
        })

        axios.post('/api/sendmail', data)
        .then(res => {
            if(res.data.result !=='success') {
                setData({
                    ...data,
                    buttonText: 'Failed to send',
                    sent: false,
                    err: 'fail'
                })
                setTimeout(() => {
                    resetForm()
                }, 6000)
            } else {
                setData({
                    ...data,
                    sent: true,
                    buttonText: 'Sent',
                    err: 'success'
                })
                setTimeout(() => {
                    resetForm();
                }, 6000)
            }
        }).catch( (err) => {
            //console.log(err.response.status)
            setData({
                ...data,
                buttonText: 'Failed to send',
                err: 'fail'
            })
        })
    }

Let's go through what this function does. After preventing the default behaviour of the form, the form sets the buttonText item of the data object to 'Sending...'. We will use this to change the text on the submit button and give the user some feedback.

Next, the function perfoms and axios.post request to the url api/sendmail which will call our Netlify function when we buid that. If the response is anything but 'success' the button text will be changed to 'Failed to send' and our utility item err will be set to 'fail' for use later. The form then resets after 6 seconds with the setTimeout function.

If the response is 'success' then the button text is changed to 'Sent' and err item changed to 'success'. We then deal with any request-related errors in the same way within the catch clause.

You'll notice that we reference a resetForm function. And here it is:

    const resetForm = () => {
        setData({
            name: '',
            email: '',
            message: '',
            sent: false,
            buttonText: 'Submit',
            err: ''
        });
    }

This function sets the data object back to its original state.

We then just need to change the onClick and value attributes of our button to call the handleSubmit function and update the button text accordingly:

<Button variant="contained" color="primary" onClick={formSubmit}>{data.buttonText}</Button>

Netlify Functions

Netlify functions allow you to write APIs that give your apps server-side functionality. In our case we are going to write a function that will take our data object as a post request and use nodemailer to send an email to a recipient.

The first thing I would suggest is to install the Netlify CLI by running npm install netlify-cli -g. This wil help us to test our form. Then we create a directory called functions in our project root (you don't have to call it 'functions'). In functions folder, create a file called sendmail.js. Notice something? Our axios.post requests posts to api/sendmail - this is important the post location and the function filename need to be the same.

By this point, Netlify CLI should have installed, so we grab a copy of nodemailer which is a free Node.js module that, in their words, allows 'easy as cake email sending'. Everyone loves cake. Run npm install nodemailer.

While that's installing we head into our sendmail.js file and add this code:

const nodemailer = require('nodemailer');

exports.handler = function(event, context, callback) {

    let data = JSON.parse(event.body)

    let transporter = nodemailer.createTransport({
        host:[YOUR SMTP SERVER],
        port:[YOUR SMTP SERVER PORT],
        auth:{
         user:[YOUR SMTP SERVER USERNAME],
         pass: [YOUR SMTP SERVER PASSWORD]
    }
    });

    transporter.sendMail({
        from: [YOUR SMTP SERVER EMAIL ADDRESS],
        to: [RECIPIENT EMAIL ADDRESS],
        subject: `Sending with React, Nodemailer and Netlify`,
        html: `
            <h3>Email from ${data.name} ${data.email}<h3>
            <p>${data.message}<p>
            `
    }, function(error, info) {
        if (error) {
            callback(error);
        } else {
            callback(null, {
            statusCode: 200,
            body: JSON.stringify({
                   'result': 'success'
                })
        });
        }
    });
}

What does this function do, I hear you ask? Netlify functions are all setup in the same way and are documented extensively. In short they export a handler method and take event, context and callback parameters. In our case we use the event and callback parameters which are the equivelant of request and response.

First the function parses the request data object. Next we declare and setup a transporter variable which holds data related to the SMTP transport we are using. Nodemailer requires the SMTP server, port and authentication information of your chosen SMTP transport. I used Zoho mail which is free, but you can use any provider (Hotmail, Outlook, ...). You can use Gmail which seems a popular choice but there are documented issues with using Gmail so you may want to use another provider.

You can read more about nodemailer SMTP transport here. There is a list of well-known SMTP services that work with nodemailer here.

Back to the function. Once the transporter variable is setup we use transporter.sendMail(data[, callback]) to configure our message and send the email.

Setting Redirects

We need to do a few final bits to get this up and running. First, we need to create a netlify.toml file in our project root. This file let's Netlify know what the build configuration is and where any functions are located. In our netlify.toml file we add two crucial pieces of configuration:

[build]
    functions = "functions"
[[redirects]]
    from = "/api/*"
    to = "/.netlify/functions/:splat"
    status = 200

The first is a build command that tells Netlify that our functions are in the functions directory. Simple.

The second is a redirect that tells Netlify to redirect anything posted to /api/* should be redirected to our function in the /.netlify/functions/ directory. The :splat keyword tells Netlify for match anything that follows the asterisk (*), so anything posted to /api/sendmail/ would be redirected to /.netlify/functions/sendmail, and look sendmail just happens to be the name of our function file. So our posted data will end up in our function as expected. You can read more about Netlify redirects here

Test Deploy

Because we have installed Netlify CLI, it's easy to test our form by running netlify dev in our terminal. This will run a local copy of our contact form.

Conclusion

I've added some basic validation to the form as well as react-google-captcha. You can check out all the code in this repo. For the Netlify function, I modified the code found in this repo. There are a lot of Netlify functions example code snippets here too.

Cover photo credit: Photo by Brett Jordan on Unsplash

Top comments (11)

Collapse
 
sachafrosell profile image
Alexander Frosell

can get this working on netlify dev but not on the deployed version... Any suggestions. It also does not work when running a standard npm start

Collapse
 
ishamjassat profile image
Isham Jassat

Strange - do you have more detail? E.g. any error messages, the Netlify function logs etc? Link to a repo?

Collapse
 
sachafrosell profile image
Alexander Frosell

I basically just get a 404 post error even when running with npm start. Works perfectly when using netlify dev

Thread Thread
 
ishamjassat profile image
Isham Jassat

Ok, first thing I would suggest is to check your netlify.toml file and make sure the redirect is setup correctly:

[build]
functions = "functions"
[[redirects]]
from = "/api/*"
to = "/.netlify/functions/:splat"
status = 200

But without seeing your code I’m a bit in the dark. So if you post your code on stackoverflow and send me a link to the question, or a direct link to your GitHub repo I can help troubleshoot

Thread Thread
 
ishamjassat profile image
Isham Jassat

I guess the other thing to try is to reference the fully qualified production URL - https://[your app name].netlify.app/api and see if that works

Collapse
 
erincodes profile image
Erin Murphy

Thank you for this! So far so good, however, I am still yet to deploy my application.

I added another transporter.sendMail() function that sends the email submission to the sender so they can see the message they sent using the contact form, it works perfectly!

Collapse
 
mave838 profile image
maverick • Edited

It is working correctly with outlook. Sends the email. But when I press submit on the console I see 500 error [HTTP/1.1 500 Internal Server Error 195ms]

lambda response was undefined. check your function code again
Response with status 500 in 163 ms.

Collapse
 
iraqwarvet31 profile image
Larry

Mine works locally no problem but not in production. Getting this error:

createError.js:16 Uncaught (in promise) Error: Network Error
at createError (createError.js:16)
at XMLHttpRequest.handleError (xhr.js:84)

Collapse
 
ikeeptrying profile image
Dre

I wanted to see the code, but your link just goes to a repo with a copy of a 'create-react-app' that has not been touched. ???

Collapse
 
adebayoomolumo profile image
Adebayo Omolumo

I also keep getting error while using the JSON.parse function on "exports.handler"

Collapse
 
adebayoomolumo profile image
Adebayo Omolumo

Could you provide more info in the netlify api scripts? I have a similar code but I am stuck trying to post to the backend then to a db using netlify.