Nitin Ranganath I'm a computer engineering student and an avid full-stack developer who loves to build for the web and mobile. I create user-centric websites with React, TypeScript, Node.js, and other JavaScript technologies.

Creating contact forms with the Notion API and Next.js

8 min read 2500

Building Contact Pages With The Notion API And Next.js

Whether it’s your portfolio website or a SaaS product website, you need a dedicated contact page for visitors and potential customers so they can reach out to you. Usually, this page includes a form that visitors can fill out to send a message or ask a question. But where and how do you store these responses?

Setting up a database for the sole purpose of storing form submissions isn’t the most convenient option. With Next.js and the Notion API, you can use the database feature to save all submissions directly to your Notion workspace. This is especially useful if you already use Notion as a project management tool and can bring everything together in one place.

Prerequisites

You’ll need a basic understanding of Next.js (or React) for this tutorial. All code that we’ll be writing in the Next.js API routes can also be used in your Node.js server in case you’re not using Next.js.

In this tutorial, we’ll cover:

  • Setting up a Notion database
  • Building a contact page with Next.js
  • Styling the page with CSS modules
  • Using environment variables on Next.js
  • Using Next.js API routes for interacting with the Notion API via SDK

Setting up the Notion database

First, let’s set up our Notion database and create a new integration to access it via Notion API. Visit the Notion website and start by creating a new page for the database. You might be prompted to log in if you’ve never used Notion before.

Dashboard To Create A Contact Page With Table Database
Creating a new Notion page with table database

On the bottom left corner of your screen, click + New Page and name it whatever you prefer. I’ll name it “Contact Form Submissions” for now. Now, click on Table in the database section to create a tabular database for storing form responses.

Dropdown Tab To Create Property Modifications To Database

You can modify the database for your requirements by adding new properties and/or editing existing ones to the table. Here are the properties and their types we’ll be storing in our Notion database:

  1. Name: title property type
  2. Email: email property type
  3. Purpose: select property type
  4. Message: text property type
  5. Contacted On: creates a time property type that is auto-generated when the form submission is added to the table

Now that our Notion database is ready, let’s create a new integration to connect the database to our Next.js application.

Creating an integration for Notion and Next.js

To create a new integration, go to Notion’s My Integrations page and click +New Integration. You’ll need to fill out basic information and select the workspace in which you created the contact page. Once you’re finished, you’ll be redirected to a screen like this:

My Integrations Section In Notion Database
Notion integrations screen

Copy and store the internal integration token somewhere safe, as we’ll need it later when we interact with the Notion API. Do not share this token with anyone else.

Search Bar to Connect Notion Database To Integration
Connecting the integration with page

Finally, to connect the Notion database to our newly created integration, return to the Contact Form Submissions page and click on Share, located on the top-right corner of your screen. Select your integration and click Invite to allow it edit access.

That’s it for the configuration part. Time to move on to the code.

Setting up the Next.js application

You can use the create-next-app tool to quickly bootstrap a new Next.js application by running this command on your terminal:

npx create-next-app
# or
yarn create next-app

Next, open the project folder on your favorite text editor or IDE. Let’s install a couple of npm packages before running the Next.js development server.

We’ll use two packages: react-toastify to display toast notifications on form submissions and @notionhq/client for using the Notion JavaScript SDK to interact with the Notion API.

npm install react-toastify @notionhq/client
# or
yarn add react-toastify @notionhq/client

Start the development server by running npm run dev on your terminal. On visiting http://localhost:3000, you will see a screen like this:

Welcome Page Of Next.js Inviting Viewer To Get Started
Next.js boilerplate code output

Building and styling the contact form

Let’s replace the boilerplate code generated by the create-next-app tool with our custom contact page with a form. Open the pages/index.js file and paste the given below:

import styles from '../styles/Home.module.css';

export default function Home() {
  return (
    <div className={styles.container}>
      <form className={styles.form}>
        <h1 className={styles.title}>React Out To Us</h1>
        <div>
          <label htmlFor="name">Full Name</label>
          <input
            type="text"
            id="name"
            name="name"
            placeholder="John Doe"
            required
          />
        </div>
        <div className={styles.inputs}>
          <div>
            <label htmlFor="email">E-Mail Address</label>
            <input
              type="email"
              name="email"
              placeholder="[email protected]"
              required
            />
          </div>
          <div>
            <label htmlFor="purpose">Purpose</label>
            <select name="purpose" id="purpose">
              <option value="" selected disabled required>
                Select one
              </option>
              <option value="Web Development">Web Development</option>
              <option value="App Development">App Development</option>
              <option value="Query / Question">Query / Question</option>
              <option value="Feedback / Message">Feedback / Message</option>
            </select>
          </div>
        </div>
        <div>
          <label htmlFor="message">Message</label>
          <textarea
            name="message"
            id="message"
            rows="5"
            placeholder="Hi there!"
            required
          ></textarea>
        </div>
        <button className={styles.btn} type="submit">
          Submit
        </button>
      </form>
    </div>
  );
}

Feel free to modify the inputs, and make sure your Notion database also has the same inputs. Now let’s dress up the form with some CSS.

Go to styles/Home.module.css and replace the styles in that file with the styles given below:

.container {
  height: 100vh;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
}
.form {
  background: white;
  padding: 2rem 1.5rem;
  border-radius: 6px;
  box-shadow: 2px 4px 8px rgba(0, 0, 0, 0.1);
  max-width: 600px;
}
.title {
  margin: 0 0 1.5rem;
}
.form label {
  display: block;
  text-transform: uppercase;
  font-size: 0.9rem;
  margin-bottom: 0.5rem;
  color: #334155;
}
.form input,
.form select,
.form textarea {
  width: 100%;
  border: none;
  background: #f4f4f5;
  padding: 0.75rem 0.5rem;
  font-size: 1rem;
  margin-bottom: 1.25rem;
  border-radius: 4px;
}
.form input:focus,
.form select:focus,
.form textarea:focus {
  outline: 2px solid #0093e9;
}
.inputs {
  display: flex;
  justify-content: space-between;
}
.inputs div {
  flex: 1;
}
.inputs div:first-child {
  margin-right: 1rem;
}
.btn {
  background-color: #0093e9;
  background-image: linear-gradient(160deg, #0093e9 0%, #80d0c7 100%);
  padding: 0.5rem 1rem;
  border: none;
  color: white;
  font-size: 1rem;
  font-weight: bold;
  border-radius: 4px;
  box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
  cursor: pointer;
}

Just like the inputs, you can play around with the styles to customize them to your liking. Visit http://localhost:3000, and you’ll see a page similar to this:

Contact Page With Blue Background
Contact page and form

Not bad! We’ve yet to set up the input states and the form submission handler, but we’ll come back to it later after setting up our API routes.



Storing confidential data in environment variables

It’s good practice to store all sensitive information — such as your Notion internal integration token and Notion database ID — as environment variables so you can easily change them later when required and to prevent exposing them to the browser.

So, create a new file named .env.local inside the root directory of the project. Inside this file, we’ll be storing the NOTION_API_KEY variable, which is the internal integration token, and NOTION_DATABASE_ID, which we can get from the URL of the Notion database.

The URL to your Notion database might look something like this.

Highlighted Text In URL Of Contact Page
The select text in the URL denotes the database ID

The database ID is the alphanumeric before ?v=, which is 491b722c931a42208cfff667dcb58a12.

Make sure to use your own integration token and database ID. Here’s how your .env.local file should look:

NOTION_API_KEY = secret_qidbxxxxxxxxxxxxxxxxxxxxMYitF6IM
NOTION_DATABASE_ID = 491b722c931a42208cfff667dcb58a12

Interacting with the Notion API via Next.js API routes

Go to the pages/API folder and rename the hello.js file to submit-form.js. The API route for this file will be available at http://localhost:3000/api/submit-form. We’ll use Notion’s official JavaScript SDK to interact with our Notion database.

First, we need to import Client from the @notionhq/client SDK package and create a new instance of it. This instance takes an object with the auth key’s value set to the integration token, which can be accessed using process.env.NOTION_API_KEY.

In the handler function, the HTTP request method can be accessed using req.method. Because we’re only anticipating POST requests from our Next.js frontend, we can respond with 405 Method Not Allowed for other types of requests.

const { Client } = require('@notionhq/client');

const notion = new Client({
  auth: process.env.NOTION_API_KEY,
});

export default async function handler(req, res) {
  if (req.method !== 'POST') {
    return res
      .status(405)
      .json({ message: `${req.method} requests are not allowed` });
  }
  try {
    const { name, email, purpose, message } = JSON.parse(req.body);
    await notion.pages.create({
      parent: {
        database_id: process.env.NOTION_DATABASE_ID,
      },
      properties: {
        Name: {
          title: [
            {
              text: {
                content: name,
              },
            },
          ],
        },
        Email: {
          email: email,
        },
        Purpose: {
          select: {
            name: purpose,
          },
        },
        Message: {
          rich_text: [
            {
              text: {
                content: message,
              },
            },
          ],
        },
      },
    });
    res.status(201).json({ msg: 'Success' });
  } catch (error) {
    res.status(500).json({ msg: 'There was an error' });
  }
}

To add a new form response to our Notion database, use the notion.pages.create() method. If you haven’t used Notion before, it is worth noting that each database entry is a page in Notion. This method takes an object with the parent object and properties object as a parameter.

In the parent object, set the database_id to your Notion database’s ID, which can be accessed via process.env.NOTION_DATABASE_ID. The properties object may seem complicated at first glance, but the documentation for property value object has examples for various property types, such as rich_text, number, title, select, and more.

Finally, enclose the entire notion.pages.create() method inside a try…catch block to catch errors and respond accordingly. Your API route is now ready to interact with your Notion database.


More great articles from LogRocket:


Adding functionality to the contact form

Although the contact form is ready in terms of layout and design, we’ve yet to add functionality to it. Let’s do that now by:

  1. Adding state to form inputs using the useState() hook
  2. Creating a submit handler for the form that will call our API route
  3. Displaying toasts notifications according to the response

Let’s code these functionalities in pages/index.js.

// For handling input states
import { useState } from 'react';

// For display toasts  
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.min.css';

import styles from '../styles/Home.module.css';

export default function Home() {

  // Input states
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');
  const [purpose, setPurpose] = useState('');
  const [message, setMessage] = useState('');

  // Form submit handler
  const submitForm = async (e) => {
    e.preventDefault();
    const res = await fetch('http://localhost:3000/api/submit-form', {
      method: 'POST',
      body: JSON.stringify({ name, email, purpose, message }),
    });
    // Success if status code is 201
    if (res.status === 201) {
      toast('Thank you for contacting us!', { type: 'success' });
    } else {
      toast('Please re-check your inputs.', { type: 'error' });
    }
  };

  return (
    <div className={styles.container}>
      <ToastContainer />
      <form className={styles.form} onSubmit={submitForm}>
        <h1 className={styles.title}>React Out To Us</h1>
        <div>
          <label htmlFor="name">Full Name</label>
          <input
            type="text"
            id="name"
            name="name"
            placeholder="John Doe"
            value={name}
            onChange={(e) => setName(e.target.value)}
            required
          />
        </div>
        <div className={styles.inputs}>
          <div>
            <label htmlFor="email">E-Mail Address</label>
            <input
              type="email"
              name="email"
              placeholder="[email protected]"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              required
            />
          </div>
          <div>
            <label htmlFor="purpose">Purpose</label>
            <select
              name="purpose"
              id="purpose"
              value={purpose}
              onChange={(e) => setPurpose(e.target.value)}
            >
              <option value="" disabled required>
                Select one
              </option>
              <option value="Web Development">Web Development</option>
              <option value="App Development">App Development</option>
              <option value="Query / Question">Query / Question</option>
              <option value="Feedback / Message">Feedback / Message</option>
            </select>
          </div>
        </div>
        <div>
          <label htmlFor="message">Message</label>
          <textarea
            name="message"
            id="message"
            rows="5"
            placeholder="Hi there!"
            value={message}
            onChange={(e) => setMessage(e.target.value)}
            required
          ></textarea>
        </div>
        <button className={styles.btn} type="submit">
          Submit
        </button>
      </form>
    </div>
  );
}

For handling input states, we can use the useState() hook and assign each input a value and an onChange handler accordingly to make it a controlled input.

Now create an asynchronous function named submitForm that makes a POST request to our API route at http://localhost:3000/api/submit-form using fetch(). The input states can be sent in the body.

Once the request has been made, we can check the status code of the request. Status code 201 denotes that the response has been successfully added to the Notion database. Otherwise, it will produce status code 500, which denotes an error.

For adding toasts, import the toast() function and the ToastContainer component from react-toastify. Additionally, import the CSS file from react-toastify/dist/ReactToastify.min.css to style them.

Add the <ToastContainer /> component to the JSX and use the toast() function to dispatch toasts based on the response received from the API inside the submitForm function.

Testing the form

Let’s test our contact form to see if it’s working. Fill out the form with proper inputs. You’ll notice a successful toast on submitting the form.

Contact Form Submitted With A Green Confirmation Message
Successful form submission

On submitting an incorrect response, such as an invalid purpose, the error toast will pop up.

Contact Form Attempted To Submit With Red Unsuccessful Messages
Unsuccessful form submission

You can verify this with your Notion database to confirm that the responses are stored as intended.

Dashboard In Notion Showing Responses From Users
Form responses in Notion

Conclusion

Notion is a powerful tool for teams and individuals alike. With the Notion API, you won’t ever need to set up a database or use a third-party service to manage your contact form responses. You can even share it with your team and visualize the responses in multiple ways — for free! I hope you enjoyed this tutorial.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next.js app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Nitin Ranganath I'm a computer engineering student and an avid full-stack developer who loves to build for the web and mobile. I create user-centric websites with React, TypeScript, Node.js, and other JavaScript technologies.

Leave a Reply