DEV Community

Cover image for Tailwind CSS Contact Form with a node.js Form Handler - Part 2
Andy Griffiths
Andy Griffiths

Posted on

Tailwind CSS Contact Form with a node.js Form Handler - Part 2

If you followed along with part 1 of this mini-series, you should have a beautiful contact form styled with Tailwind CSS.

In Part 2, we’re going to build the back-end to handle the form submissions using node.js, Express and SendGrid.

Download the SOURCE CODE

As we did in part 1, we’re going to be using npm to manage our dependencies.

Install Our Dependencies

Create a new project folder mkdir form-handler, set this as your current directory cd form-handler and then run npm init -y to create our package.json file which we will need for our dependencies.

Let's now pull in all the dependencies we're going to need. Run npm i express @sendgrid/mail dotenv. We also need a dev dependency, so run npm i -D nodemon.

If that all went a bit over your head, don’t worry, I’ll explain what just happened.

In your project directory, you should now see a new directory node_modules - this contains all of the dependencies (third party code) we are going to utilise in our project.

If you open up your package.json file, you will see we have 3 dependencies under dependencies and 1 under devDependencies. The first 3 will be utilised in our final code base, the devDependencies is only used for development purposes.

If you want to learn more about what each of these dependencies does beyond the scope of this tutorial, then check the links below.


Now we have everything installed, there are a couple more things we need to do before we can write the actual code to build the contact form handler.

Setup and Configure SendGrid

In order to be able to email the form responses, we need a mail service that can handle the form request. In this case, we’re going to use SendGrid.

If you haven't already, head over to SendGrid and register for a free account.

Once you have access to your account, from the dashboard you will need to create an API Key. Click on Settings > API Keys and then select Create AP Key.

For the purpose of this tutorial, we only need limited access to the API, so name your key something suitable, so you remember what it's for and then select Restricted Access. Allow access just for Mail Send, then click Create & View.

You will then be presented with your API Key. Make sure you copy this somewhere safe, as they only show you this once.

You will also need to verify your sender identity to be able to send any emails.

Now we have our API Key and have verified our sender identity, head back to our app and let’s create the file we need to hold this API key. Create a .env file in the root of your project touch .env.

Open up the file and add our key like so:

// .env
Enter fullscreen mode Exit fullscreen mode

Building the Mail Server

Next, let’s create our app.js file with touch app.js and then open it up.

To be able to use the SendGrid API, we will need to import the SendGrid library, access the SendGrid API Key variable from within our .env config file and then tie them together.

// app.js
const sgMail = require('@sendgrid/mail');
Enter fullscreen mode Exit fullscreen mode

While we’re at it, we may as well import the Express library and set this up.

// app.js
const express = require('express');
const app = express();
Enter fullscreen mode Exit fullscreen mode

To make things easier to configure when we push this to production, we will also add a PORT variable to our .env config file and pull this in to our app.js.

// .env
Enter fullscreen mode Exit fullscreen mode
// app.js
const port = process.env.PORT || 3000;
Enter fullscreen mode Exit fullscreen mode

We’re now ready to set up a simple server and add a route using Express, which will allow us to accept a POST request to our app which we can then use to handle the form submission.

In our app.js file we have access to Express with the app variable, so let’s use that to set up the server and create our route.

// app.js
app.listen(port, (error) => {
    if (error) throw error;
    console.log(`Listening on port ${port}`);
});'/’, (req, res) => {
    // handle form submission
Enter fullscreen mode Exit fullscreen mode

To be able to parse the data we receive from the form submission we need to add a couple of middlewares. Don’t worry too much if you don’t understand what a middleware is or what it does, just know we need these 2 below.

app.use(express.urlencoded({ extended: true }));
Enter fullscreen mode Exit fullscreen mode

At this point, if we run node app.js we should get a console log telling us that our app is Listening on port 3000. This tells us that the server is running. However, we don’t want to have to continually start up the server every time we make a change in our app.

This is why we installed the nodemon package as a dev dependency earlier.

To set this up, we need to add a couple of custom scripts to our package.json file..

// package.json
  "scripts": {
    "prod": "node app.js",
    "dev": "nodemon app.js"
Enter fullscreen mode Exit fullscreen mode

Notice we added one for prod and one for dev - they are very similar apart from one uses node and the other nodemon.

When building our app locally, we can spin up the server using npm run dev. This will allow us to edit our app without having to constantly restart the server. This is because it uses the nodemon package which we set up in the package.json file.

So let’s run that now npm run dev. You should see a couple of nodemon message and then Listening on port 3000, this shows it is working.

At the minute we don’t have any code inside of our post route - let’s take a look at that next.

From part 1 of this tutorial, we created a contact form styled with TailwindCSS. If you look back at this code, you will see there are 3 input fields that we need to pull the data from message, name & email.

When a user submits the forms, these values will be POSTed to our mail server and we can then access these from the request using req.body.

Using destructuring, we can set these values to local variables in our app.

const {
} = req.body;
Enter fullscreen mode Exit fullscreen mode

This effectively looks at the request body and assigns the data to variables with corresponding names.

We can then use the data stored in these variables to compose an email to send to ourselves to get the form submission. We will use template literals to easily allow us to embed variables within our text.

    const msg = {
        to: '', // Change to your recipient
        from: '', // Change to your verified sender
        subject: 'Contact Form Submission',
        text: `Message: ${message} Name: ${name} Email: ${email}`,
        html: `
Enter fullscreen mode Exit fullscreen mode

Now we have our data ready to send to SendGrid, let’s use sgMail to do just that.

        .then(() => {
        console.log('Email sent')
        .catch((error) => {
Enter fullscreen mode Exit fullscreen mode

We should now have a working simple mail server that can handle our submissions from our contact form.

If we’re going to use this mail server in a production environment, then we would need to implement both client-side and server-side validation to make sure we are sending, receiving and handling correctly formatted and safe data.

For now, this is beyond the scope of this tutorial. I may add a third part to the series covering this if there's enough interest.

Sending a Contact Form Request

Open up the contact form we created in part 1 of the series and update the form action to point to our mail server API and add the method attribute set to post.

<form action="http://localhost:3000" method="post">
Enter fullscreen mode Exit fullscreen mode

Make sure in your HTML that the <button> type is set to submit.

<button type="submit">
Enter fullscreen mode Exit fullscreen mode

Make sure our node server is running with npm run dev and then open the contact form in our browser. Add some dummy data to the form fields and hit Send...

...if everything worked correctly, we should see an Email sent message in our console and have received an email with the data we sent. If you have not received the email, check your JUNK folder.

You may have noticed that the form has redirected to http://localhost:3000/. This isn’t great. Ideally, we would redirect back to the original page the form was posted from, or a dedicated success page.

We could hard code the redirect URL into the Express route, but this isn't that flexible. Instead, we'll add this via a small piece of JavaScript in our contact form.

Inside our <form> tag, at the top, add a hidden field - this will hold the URL of the page we submit the form from.

<input type="hidden" name="redirect" class="redirect">
Enter fullscreen mode Exit fullscreen mode

Now let’s write the JavaScript to populate the hidden field we’ve just added. At the bottom of our code, just inside our <body> tag add a script tag with the following code.

    const redirectField = document.querySelector('.redirect');
    const pageUrl = window.location.href;
    redirectField.value = pageUrl;
Enter fullscreen mode Exit fullscreen mode

If you inspect your HTML with your developer tools, you should now see the redirect input has a value of something like We now need to pull this in to our mail server.

Update our form handler code to add the redirect value.

const {
} = req.body;
Enter fullscreen mode Exit fullscreen mode

We can now change the res.end() to res.redirect(redirect) in our send email code.

        .then(() => {
        console.log('Email sent')
        .catch((error) => {
Enter fullscreen mode Exit fullscreen mode

This now redirects us back to the page we submitted the form from but from a user experience point of view it is still lacking in terms of any feedback as to whether the form has been submitted successfully. Let’s fix that.

Instead of just redirecting back to the exact URL we got from our redirect variable, we could use a query string to tell the user their form was a success.

Change res.redirect(redirect) to res.redirect(${redirect}?submit=true).

Now if we submit a form successfully, our mail server will forward us back to our original URL with an additional query string ?submit=true. We now need to capture this in our form code, so we can show a success message.

Inside our <form> tag right at the top, add an empty div

<div class="form-message"></div>
Enter fullscreen mode Exit fullscreen mode

Then add the JavaScript to handle this.

const queryString =;
const urlParams = new URLSearchParams(queryString);
const formMessage = document.querySelector('.form-message');

if(urlParams.has('submit')) {
    if(urlParams.get('submit') === 'true') {
        formMessage.innerHTML = `<div class="mb-5 p-3 max-w-xl mx-auto shadow-md sm:border-0 md:border md:border-gray-900 md:dark:border-gray-100 bg-green-400 dark:bg-green-400 text-gray-900 dark:text-gray-900">Thanks for your enquiry, someone will be in touch shortly.</div>`;
        window.scrollBy(0, -20);
Enter fullscreen mode Exit fullscreen mode

Now, whenever the redirect includes ?submit=true you will be presented with the success message. As we have added new classes to our HTML, we will need to make sure we run npm run build to make sure these styles are included in the output of our CSS file.

That wraps it up.

You should now have a fully functioning contact form that can email the submissions to your email address. There are other considerations to make when building publicly accessible forms, like validation, sanitisation etc.

I may look to add a third part to the series, which will cover this in more detail.

Top comments (2)

gabenedden profile image

Thanks so much for writing up this post. I'm building a site using the react router and tailwind. Instead of trying to force this into my current project, would I be able to host this handler on an entirely seperate server and then just send the request from my current app to that server?

For example, having your App.js served on a heroku site, then just POST to that URL from my current site.

Thank you for your time!

brandymedia profile image
Andy Griffiths

Yes of course you can. The code is freely available on Github.

Feel free to use this as you wish 👍

BTW - sorry for the (really) late reply - only just seen this comment.