How to build an email contact form with SendGrid and Node.js

March 21, 2022
Written by
Phil Nash
Twilion
Reviewed by

How to build an email contact form with SendGrid and Node.js

Displaying your email address on a website can result in your email being scraped and used for spam. One way to get around this, but still allow people to contact you from your website, is to build a contact form.

In this post you will build a contact form using SendGrid to deliver emails to your inbox without exposing your email address.

You will build the project with Twilio Functions, but you could adapt the code to use in any Node.js environment.

How is this different to sending an email with an API?

When someone fills in a contact form, you might expect to receive an email in your inbox from their email address. However, to maintain a good email sending reputation, SendGrid only allows you to send emails from addresses that you have verified individually or from domains you have authenticated.

So, instead of using the submitted email address as the from address, you can enter it as a reply-to address instead. That way, SendGrid will send the email from your authenticated email address, but when you hit the reply button in your email client the new email will be to the submitted email address.

Since you control the email sending process, you can do other things like add a [contactform] tag to the email subject so you can easily filter emails from your contact form. Or you could capture extra information and include it in the body of the email.

Now you know the considerations for building a contact form with SendGrid, let's get on and build our own.

Build a contact form

You're going to build a form that looks and works like this:

A working contact form. You enter your email address, a subject and your message then press "Send". A message pops up in green when the email is successfully sent.

 

Here is everything you'll need to start building:

What you'll need

Initiate the project

To build the server-side of our contact form, you're going to use a Twilio Function. When the project is finished, you'll be able to host the complete project in Twilio's infrastructure.

Once you have the Twilio CLI installed, install the Serverless Toolkit plugin by running this command in your terminal:

twilio plugins:install @twilio-labs/plugin-serverless

With this plugin you can create, run, and deploy your project. Create a new project with the following command:

twilio serverless:init contact-form --empty

The --empty flag generates a project without example files. Open your new project up in your text editor. Let's start with just the code you'll need to send an email, and then expand to build the full contact form experience.

Setting up the environment

You need to set up some environment variables that you will need for sending an email.

Domain authentication is the preferred method of sender verification. Though single sender verification provides a quick start, domain authentication should be completed before you launch your mail send into production. If access to DNS records is a barrier for you, you can always start developing with single sender verification and return to domain authentication once you obtain DNS access.

Open the .env file and enter those details like this:

SENDGRID_API_KEY=SG.YOUR_API_KEY
TO_EMAIL_ADDRESS=YOUR_TO_ADDRESS
FROM_EMAIL_ADDRESS=YOUR_FROM_ADDRESS

Now it's time to write some code to send your first email.

Sending an email

To send an email with SendGrid, you'll first install the SendGrid Node helper library. In your project directory, run the following command:

npm install @sendgrid/mail

Now, create a file in the functions directory called send-email.js and open it. Start by requiring the helper library you just installed:

const sg = require("@sendgrid/mail");

This file will act as a Twilio Function, so you need to create a handler function that receives 3 arguments: context, event, and callback:

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

Now, the code to send the email. Back in send-email.js, you need to configure the SendGrid library with the API key. The context object contains all of your environment variables from the .env file. Then, build an object with the properties to, from, subject, and text.


exports.handler = async function(context, event, callback) {
  sg.setApiKey(context.SENDGRID_API_KEY);
  const msg = {
    to: context.TO_EMAIL_ADDRESS,
    from: { email: context.FROM_EMAIL_ADDRESS, name: "Your contact form" },
    subject: "New email",
    text: "This is a brand new email.",
  };
}

To send the email, pass this object to the send function of the sg object. This is an asynchronous operation, so await the result and wrap the whole thing in a try/catch block.


exports.handler = async function(context, event, callback) {
  sg.setApiKey(context.SENDGRID_API_KEY);
  const msg = {
    to: context.TO_EMAIL_ADDRESS,
    from: { email: context.FROM_EMAIL_ADDRESS, name: "Your contact form" },
    subject: "New email",
    text: "This is a brand new email.",
  };
  try {
    await sg.send(msg);
    return callback(null, "Email sent!");
  } catch (error) {
    console.error(error);
    return callback(error);
  }
}

This is all the code you need to send an email. In the terminal, run the app with this command:

npm start

Then open the URL http://localhost:3000/send-email, this will run the function. If you get the message "Email sent!" check your inbox, you should have received an email!

If there's an error, check the console to see more information. Alternatively, if the code did run successfully but you did not receive the email, check the Email Activity page in your SendGrid dashboard to see what happened.

Create the contact form

So far this code sends the same email every time you request the URL. To build a contact form, you need a front-end that will take some input, like the subject, content of the email, and the email address of the person sending it. You then need to take that user input and change your existing code to send the email with the submitted content and make the submitted email the reply-to address in your email.

Start by creating a file called index.html in the assets directory, and enter this code:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Contact form</title>
    <link rel="stylesheet" href="./style.css" />
  </head>
  <body>
    <main>
      <h1>Contact Form</h1>
      <form action="/send-email" method="POST" id="contact-form">
        <div class="form-field">
          <label for="email">Your email address</label>
          <input
            type="email"
            inputmode="email"
            name="from"
            id="from"
            required
          />
        </div>
        <div class="form-field">
          <label for="subject">Subject</label>
          <input type="text" name="subject" id="subject" required />
        </div>
        <div class="form-field">
          <label for="content">Your message</label>
          <textarea name="content" id="content" required></textarea>
        </div>
        <div class="actions">
          <button type="submit" id="submit-button" class="button-primary">
            Send
          </button>
        </div>
      </form>
      <p id="status" class="status" hidden></p>
    </main>
  </body>
</html>

This is an HTML page that includes a form with inputs for the email address and the subject, and a textarea for the content of the email. Submitting the form makes a POST request to your /send-email function.

Let's add a bit of style to this to make it a little easier on the eye. Create a file in the assets directory called style.css and add the following code to the file:

* {
  box-sizing: border-box;
}

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
        Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  padding: 10px;
}

main {
  max-width: 800px;
  width: 100%;
  margin: 0 auto;
}

form label {
  display: block;
}

form input,
form textarea {
  width: 100%;
  font-size: 16px;
}

form button {
  font-size: 24px;
}

.form-field {
  margin-bottom: 1em;
}

.status {
  color: #fff;
  padding: 0.6rem;
  border-radius: 4px;
}

.status.success {
  background-color: rgb(20, 176, 83);
}

.status.error {
  background-color: rgb(214, 31, 31);
}

This makes the page a bit nicer (not much, but a bit!) to look at.

Now, you need to fix up your function to use the data submitted to the form to send the email. Open functions/send-email.js again.

The data from the form will be passed into the function in the event object. Update the code to create the email object to the following:

  const msg = {
    to: context.TO_EMAIL_ADDRESS,
    from: { email: context.FROM_EMAIL_ADDRESS, name: "Your contact form" },
    replyTo: event.from,
    subject: `[contactform] ${event.subject}`,
    text: `New email from ${event.from}.\n\n${event.content}`,
  };

You're still sending to your own email address and from your authenticated/verified address, but the other properties are updated like so:

  • You've added a replyTo field, which will be the user submitted email address
  • The subject comes from the subject submitted in the form, with an additional tag [contactform] which you can use to filter these emails in your inbox
  • The text content of the email now uses the email to show who submitted the form and is then filled with the text content from the form.

You need to update the way you respond from this function too. Right now it just returns a string or an error object. That's not very user friendly. Instead, respond with a JSON object that you can use in your interface to show success or failure. Before you try to send the email, create a response object:


  const response = new Twilio.Response();
  response.appendHeader("Content-Type", "application/json");
  try {
    await sg.send(msg);

When the message is sent successfully, return a successful 200 status and a JSON object that reports the success:


  try {
    await sg.send(msg);
    response.setStatusCode(200);
    response.setBody({ success: true });
    return callback(null, response);
  }

If there's an error, return an error response with a 400 status and send back the error from the API.

hl_lines="3 4 5 6 7  8 9 10 11"
  } catch (error) {
    console.error(error);
    let { message } = error;
    if (error.response) {
      console.error(error.response.body);
      message = error.response.body.errors[0];  
    }

    response.setStatusCode(400);
    response.setBody({ success: false, error: message }); 
    return callback(null, errorResponse(response, message));
  }

The whole function should look like this now:

exports.handler = async function(context, event, callback) {
  sg.setApiKey(context.SENDGRID_API_KEY);
  const msg = {
    to: context.TO_EMAIL_ADDRESS,
    from: { email: context.FROM_EMAIL_ADDRESS, name: "Your contact form" },
    replyTo: event.from,
    subject: `[contactform] ${event.subject}`,
    text: `New email from ${event.from}.\n\n${event.content}`,
  };
  try {
    await sg.send(msg);
    response.setStatusCode(200);
    response.setBody({ success: true });
    return callback(null, response);
  } catch (error) {
    console.error(error);
    let { message } = error;
    if (error.response) {
      console.error(error.response.body);
      message = error.response.body.errors[0];  
    }

    response.setStatusCode(400);
    response.setBody({ success: false, error: message }); 
    return callback(null, errorResponse(response, message));
  }
}

To top all of this off, let's add some JavaScript to the form to use the JSON response from our function.

Open assets/index.html again. After the </main> tag add a new <script></script> element.

Inside the script element you can start writing your front-end JavaScript. Start by grabbing references to the elements on the page that you will want to interact with:

const form = document.getElementById("contact-form");
const fromInput = document.getElementById("from");
const subjectInput = document.getElementById("subject");
const contentInput = document.getElementById("content");
const sendButton = document.getElementById("submit-button");
const status = document.getElementById("status");

Add a couple of functions that will help show the status of the email:

function setStatus(message, klass) {
  status.textContent = message;
  status.classList.add(klass);
  status.removeAttribute("hidden");
}
function hideStatus() {
  status.setAttribute("hidden", "hidden");
  status.className = "status";
  status.textContent = "";
}

These functions show or hide the paragraph below the form. You can use them to show success or error messages when you get the result of the form submission.

Finally, you need to handle the form submission. You'll do this by listening to the form's "submit" event, collecting the data from the form fields, then submitting it to your function using the fetch API. This is quite a bit of code, so I added comments to explain what it's doing.

// Listen for the submit event of the form and handle it
form.addEventListener("submit", async (event) => {
  // Stop the form from actually submitting
  event.preventDefault();
  // Disable the submit button to avoid double submissions
  sendButton.setAttribute("disabled", "disabled");
  // Hide any status message as we are now dealing with a new submission
  hideStatus();
  try {
    // Submit the data to our function, using the action and method from the form element itself.
    const response = await fetch(form.getAttribute("action"), {
      method: form.getAttribute("method"),
      // Collect the data from the form inputs and serialize the data as JSON
      body: JSON.stringify({
        from: fromInput.value,
        subject: subjectInput.value,
        content: contentInput.value,
      }),
      // Set the content type of our request to application/json so the function knows how to parse the data
      headers: {
        "Content-Type": "application/json",
      },
    });
    if (response.ok) {
      // If the response was a success, clear the form inputs and set a success message in the status
      fromInput.value = "";
      subjectInput.value = "";
      contentInput.value = "";
      setStatus("Message sent successfully. Thank you!", "success");
      // After 5 seconds, hide the success message.
      setTimeout(hideStatus, 5000);
    } else {
      // If the request returns an error response, read the error message from the response and set it as a failure message in the status
      const data = await response.json();
      console.log(data);
      setStatus(data.error, "error");
    }
    // The request is over, so enable the submit button again
    sendButton.removeAttribute("disabled");
  } catch (error) {
    // If the request failed, set a generic error message in the status
    setStatus(
      "There was an error and the message could not be sent. Sorry.",
      "error"
    );
    console.log(error);
    // The request is over, so enable the submit button again
    sendButton.removeAttribute("disabled");
  }
});
);

That's all the JavaScript! If you stopped the server earlier, start it again with npm start and open the page up at localhost:3000/index.html and enter an email, subject, and the content for your email. Submit the form and you should see a success message.

A working contact form. You enter your email address, a subject and your message then press "Send". A message pops up in green when the email is successfully sent.

Check your inbox and you will receive an email from your contact form. And remember, the email came from your domain, but when you reply, it will go back to the email address submitted in the form.

Your contact form, powered by SendGrid

In this example, you've seen how to build an email contact form with SendGrid, powered by Twilio Functions.

This is a Twilio Function, so you can actually deploy your application publicly to the Twilio infrastructure with the command npm run deploy.

The full code for this example can be found in the Twilio Labs function-templates repository on GitHub. You can also deploy this application from the Twilio Code-Exchange here.

An email contact form is just the start of what you can build using Twilio SendGrid, you could send images from the Mars Rover over email, receive inbound emails with the Inbound Parse webhook or even send interactive AMP emails.

I'd love to know what you're building, so drop me an email at philnash@twilio.com or send me a message on Twitter at @philnash.