Integrate a SendGrid form into a Remix app

19/02/2023

Intro and SendGrid account creation

When building this site, I wanted to have an option for people to send me a message via a contact form on the homepage. I was not sure whether anyone is ever going to use it, but it seemed like a nice addition both because it's going to fill up space (at the time, I was pretty desperate for any kind of content) and because it will introduce an integration to a third party service which I wanted to try in Remix.

The form should naturally have some error handling which we will handle on the remix backend and return either an object with errors or just a success message. We will create types for both responses and display them in the form component.

First things first, we will need to create a Sendgrid account. I chose them because I saw they have a free tier that can be used when you don't have expect many form submissions. You can actually have many more emails sent than I expected. This setup will also work with other provider but it would have to be changed slightly. In this tutorial, I will focus on using Sendgrid. You can create a free account on this URL https://signup.sendgrid.com/. Get your API keys from there, we will use them in the next step.

Disclaimer about the folder structure and missing styling

Since many people use different styling and css libraries, I will not explain the form styling here. I will keep it short and concise so that anybody can easily use this and adapt it into their own architecture and remix app. If you want to see how I implemented it here, on my homepage, you can check out the files in my repo https://github.com/lukalaz/remix-custom/blob/main/app/routes/index.tsx. In the tutorial here, it's assumed we are working with a blank remix project.

Creating the Form

In our routes folder, we create a new file, index.tsx and create the basic form:

import { Form } from "@remix-run/react";

const ContactForm: React.FC = () => {
  return (
    <Form method="post">
      <input type="text" placeholder="Enter your full name" name="name" />
      <input type="email" placeholder="Enter your Email" name="email" />
      <input type="textarea" placeholder="Enter your message" name="message" />
    </Form>
  );
};

export default ContactForm;

Now when we navigate the to root of our app, we will see a unstyled form that has 2 input fields (for the full name of the person that wants to contact us and his email address) as well as an email field so we know where to send the response to and how to contact that person in the future and one textarea for the message body.

Adding env variables and types

In our .env file, we will add our email and SendGrid api key.

...
SENDGRID_API_KEY="SG.kuF5JUL8T8GYyccAMmsNlg.1vAKT8dB8987O53EEqTKqoK771-tbSgBvUKwyxhe2MoERROR"
CONTACT_FORM_EMAIL="youremail@example.com"

Next, above the ContactForm, we will define the types ActionData, Success, and Error which will be used later.

type ActionData =
  | {
      name: null | string;
      email: null | string;
      message: null | string;
    };

type Success =
  | {
      successMessage: string;
    };

type Error =
  | {
      errorMessage: string;
    };

Now we have everything ready and together to add the function that will read the data from our form and send the request to Sendgrid!

Handling form submission

First, we need to add the Sendgrid package to our project by running npm i @sendgrid/mail. The version that I was using when building this was 7.7.0, so if something is a bit different now, check out their docs.

Next, we are adding the required imports to the top of our file:

import { ActionFunction, json } from "@remix-run/node";
import Sendgrid from "@sendgrid/mail";

Now can add the form submission function in the same file:

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();

  const name = formData.get("name");
  const email = formData.get("email");
  const message = formData.get("message");

  const errors: ActionData = {
    name: name ? null : "Name is required",
    email: email ? null : "Email is required",
    message: message ? null : "Message is required",
  };

  const hasErrors = Object.values(errors).some((errorMessage) => errorMessage);

  if (hasErrors) {
    return json<ActionData>(errors);
  }

  Sendgrid.setApiKey(process.env.SENDGRID_API_KEY || ""); // the || "" is required to pass the typechecking sa the Sendgrid function wants a string and everything coming from process.env can be undefined

  const msg = {
    to: process.env.CONTACT_FORM_EMAIL,
    from: process.env.CONTACT_FORM_EMAIL,
    subject: "A mail was sent from your websites contact form!",
    text: message?.toString(),
    html: message?.toString() || "", // the || "" is required to pass the typechecking as the html must not be undefined although it can be
  };

  const res = await Sendgrid.send(msg)
    .then(() => {
      return json<Success>({
        successMessage: "Email sent succesfully!",
      });
    })
    .catch((error) => {
      return json<Error>({
        errorMessage: error.response.body.errors[0].message,
      });
    });
  
  return res;
};

And that's it! Thank you for reading, if you have any comments on this you are free to contact me via this same form on the homepage. I will try to keep this updated, but you can always view to code on the github page for this website https://github.com/lukalaz/remix-custom/ and check whether there are any improvements.

Share this post :