Skip to main content

Getting Started with the Notion API and Its JavaScript SDK

Rui Sousa
Share

Notion is a multi-featured app for organizing all sorts of content, from notes to calendars and reminders. Notion recently launched its own API, and in this article we’ll use this API to create a small interface that will connect Notion to our own database.

Notion has released its API to the world in open beta. It has excellent documentation, it’s really easy to access and, more importantly for us JavaScript devs, it also offers an SDK for JavaScript. 🎉

While no previous knowledge is needed to follow along with this article (I’ll provide all the required steps) we will be dealing with front-end and back-end code, as there’s a bit of Node.js and Express setup involved.

Setup

Our setup will be split into two sections. The first will cover the steps that we need to follow on the Notion software and API. In the second, we’ll get our hands on the code by initializing a folder, adding the Notion dependency and creating the initial index.js and editing the package.json to make everything work.

To follow along, you’ll need a Notion account (more on that below), as well as a recent copy of Node installed on your machine. As ever, the code for the tutorial can be found on GitHub.

The Notion Setup

If you don’t already have a Notion account, please create one by following this link. It has a very generous free tier and you don’t have to add any payment information!

After creating your account and logging in, create a new page by choosing Add a page and give it a name. For this tutorial, we’ll choose the Table database. This will give us an empty table, which is exactly what we want!

The empty table that we've just created

The next step is to create some columns on our database and fill them with some mock data. For this tutorial, we’ll work just with Name and Role fields, as if we’re working with a database of employees in a company.

The table with the new fields and mock data

Now we’ll go to the documentation website. You’ll see a My integrations link in the top corner. If you click it, you’ll be directed to a screen showing “My integrations”, and yours will be empty, of course.

Notion API My Integrations page

Press Create new integration, fill in your title and be sure to choose your Associated workspace (it will be chosen by default but make sure of this). Press Submit and you’ll be directed to a new page with an Internal Integration Token (we’ll use this on our code) and with two option boxes for where you want to use your integration. You don’t need to do anything on this page other than copying your token and press Save changes.

Note: at the time of writing, there doesn’t seem to be a way to delete integrations, so name them wisely.

Notion API Create a new integration page

Notion API integration page with token

Now go back to your Notion workspace. On our newly created database, we want to press Share, then Invite. You’ll then be able to choose your newly created integration. Choose it and press Invite, and your Notion setup is done. Well done! 🙌

Notion workspace with modal for integration

The code setup

Now let’s do some code. Open your terminal and do mkdir notion-api-test (this will create a folder called notion-api-test) on your chosen location, and after that, step into your folder with cd notion-api-test and do npm init -y (this command will create a package.json with some basic setup and the -y flag answers to some prompts automatically so you don’t have to bother yourself with them).

As I mentioned before, we’re going to use notion-sdk-js, and for that we need to install it as a dependency, so we’re going to do npm install @notionhq/client.
Now, open your notion-api-test on your code editor and create an initial index.js on the root and edit the package.json scripts by replacing what’s there with the following:

"scripts": {
    "start": "node index"
},

Let’s also create a .gitignore file and another one called .env. The .gitignore allows you to put different file/folder names inside, and that means that these files/folders won’t be added to your repo when you push you code. This is very important, because our integration token(remember that?) will be inside the.env file, like this:

NOTION_API_KEY = YOUR_TOKEN_HERE

That means that inside your .gitignore you should add this on the first line:

.env

Now that we have an .env file, we should also add a new dependency, dotenv, so you can load your NOTION_API_KEY variable. You can do that by doing npm install dotenv.

The code setup is now done, and your folder should look something like what’s shown below. 🎉

How your folder structure should look

Pulling Data from the Notion API

Now that the boring part is over, let’s get to the good stuff! Our index.js file will be a Node.js file, and the following code block shows our starting code and what each line exactly does!

// this will allow us to import our variable
require("dotenv").config();
// the following lines are required to initialize a Notion client
const { Client } = require("@notionhq/client");
// this line initializes the Notion Client using our key
const notion = new Client({ auth: process.env.NOTION_API_KEY });

We also need an extra thing here, which is the ID of the database that we created on our Notion workspace. This can be obtained from the browser’s URL bar. It comes after your workspace name (if you have one) and the slash (myworkspace/) and before the question mark (?). The ID is 32 characters long, containing numbers and letters.

https://www.notion.so/myworkspace/a8aec43384f447ed84390e8e42c2e089?v=...
                                  |--------- Database ID --------|

For security purposes, you should also paste this ID into your .env file, so that it looks something like this:

NOTION_API_KEY = YOUR_TOKEN_HERE
NOTION_API_DATABASE = YOUR_DATABASE_ID_HERE

We’ll then import it into our index.js with this:

const databaseId = process.env.NOTION_API_DATABASE;

Now, to make sure that our API is working, let’s create a function that makes a call to our database. To do it, we’ll create an async function:

const getDatabase = async () => {
  const response = await notion.databases.query({ database_id: databaseId });

  console.log(response);
};

getDatabase();

If you now run npm start in your terminal, you should see a log of an object with a results property that has an array. That array contains the entries in your database. To look into them, we can do the following:

const getDatabase = async () => {
  const response = await notion.databases.query({ database_id: databaseId });

  const responseResults = response.results.map((page) => {
    return {
      id: page.id,
      name: page.properties.Name.title[0]?.plain_text,
      role: page.properties.Role.rich_text[0]?.plain_text,
    };
  });

  // this console.log is just so you can see what we're getting here
  console.log(responseResults);
  return responseResults;
};

The code above is mapping through our results (matching the entries in our database) and we’re mapping the paths for different properties to names that we’re choosing (in this case, id, name and role). Notice how specific the object path is. I’ve used optional chaining to account for blank rows in the database, or rows where one or the other of these fields isn’t filled out.

Either way, feel free to play with the different properties, and be aware that this is a matter of trial and error, and that every API behaves and organizes the information differently. The important thing here is to go through each property until we get to the info we’re looking for.

If looking into each property and using console.log() is not your thing, you could always use Postman to inspect the response. Unfortunately, that’s not within the scope of this tutorial, but you could check the “How to Master Your API Workflow with Postman” post to give it a go!

Another important note here: notice the notion.databases.query that we’ve used. If you look at the Notion API documentation, you’ll see that we’re using POST | Query a database. We could use just GET | Retrieve a database, but here I would like to challenge you to read the documentation and try to sort the list differently!

Before we wrap this part, let’s change our getDatabase function so we can properly import it into another file that we’ll create. It should look like the following:

exports.getDatabase = async function () {
  const response = await notion.databases.query({ database_id: databaseId });

  const responseResults = response.results.map((page) => {
    return {
      id: page.id,
      name: page.properties.Name.title[0]?.plain_text,
      role: page.properties.Role.rich_text[0]?.plain_text,
    };
  });

  return responseResults;
};

Setting up an Express Server

With the previous step done, we now can successfully retrieve our results. But to be able to actually use them properly, we’ll need to create a server, and the easiest way of doing so — since we’re using Node.js — is to use Express. So, we’ll get started by running npm install express and creating a new file at the root called server.js.

If express confuses you, don’t worry. We’ll be using it to facilitate our work and create a quick back end and server to our application. Without it, we wouldn’t be able to properly retrieve our data, since we’re initializing our Notion client within our code.

On our server.js file, we’ll start by importing express, the module where we have our code (index.js), our getDatabase function, a port number, and a variable to initialize our express function:

const express = require("express");
// our module
const moduleToFetch = require("./index");
// our function
const getDatabase = moduleToFetch.getDatabase;

const port = 8000;
const app = express();

// this last command will log a message on your terminal when you do `npm start`
app.listen(port, console.log(`Server started on ${port}`));

Since we’re now importing our code into a new file, server.js, we should change our start command on package.json to look for server, so it should look like this:

"scripts": {
    "start": "node server"
},

If you now run npm start, you’ll see the Server started on 8000 message, which means that our setup is working as expected! Well done!

Now that our express app is working, we need to get our database to work with it, and we can do that with app.get(). This method needs a path (it won’t matter in our case) and a callback function (which will invoke our getDatabase function):

app.get("/users", async (req, res) => {
  const users = await getDatabase();
  res.json(users);
});

The above code uses the app.get method, as referred, and inside our callback function we’re getting the results from our function and we’re using the .json() Express middleware function that parses the request into readable and workable data. (You can learn a bit more about it in the official documentation.)

This means that we’re now successfully accessing our data, and that we’ve created a route to “fetch” it. As a final step, we should add app.use(express.static("public")); to our server.js file, so that the end result looks something like this:

const express = require("express");
// our module
const moduleToFetch = require("./index");
// our function
const getDatabase = moduleToFetch.getDatabase;

const port = 8000;
const app = express();

// the code line we just added
app.use(express.static("public"));

app.get("/users", async (req, res) => {
  const users = await getDatabase();
  res.json(users);
});

app.listen(port, console.log(`Server started on ${port}`));

This last bit of code tells our back end to use a specific folder where we’ll create our front-end code, which will be the public folder. Here we’ll work with our HTML, CSS and JavaScript to access this /users route that we created on our back end. Let’s get to it!

Displaying Data from the Notion API

We’ll start by creating, at the root of our project, a folder called public. Here’s where our front-end code will live.

The HTML and CSS parts are straightforward, so I’ll mostly just leave the code here and focus on the JavaScript part, since that’s what we’re all here for!

Our HTML (/public/index.html) will look like this:

<!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>Notion API Test</title>
    <link rel="stylesheet" href="style.css" />
  </head>
  <body>
    <div id="banner">Notion API Database Test</div>
    <div id="wrapper">
      <div id="container"></div>
    </div>

    <script type="module" src="./main.js"></script>
  </body>
</html>

And our CSS (/public/style.css) will look like this:

body,
html {
  padding: 0;
  margin: 0;

  height: 100vh;
  width: 100vw;
  font-family: Arial, Helvetica, sans-serif;

  position: relative;
}

#banner {
  height: 50px;

  display: flex;
  justify-content: center;
  align-items: center;

  background-color: #ef4444;
  color: white;
  font-weight: bold;
}

#wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
  height: calc(100vh - 50px);
}

#container {
  width: 80vw;
  margin: auto;

  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
  grid-auto-rows: 200px;
  gap: 20px;
}

.userContainer {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
  border-radius: 10px;
}

If you now run npm start on your project and visit http://localhost:8000, you should see your front-end code.

Blank HTML page with a banner above

Now for our public/main.js file! Our first step is make a request to the route that we created on the back end (/users/), which will allow us to grab our database information:

const getDataFromBackend = async () => {
  const rest = await fetch("http://localhost:8000/users");
  const data = await rest.json();

  return data;
};

// Note that top-level await is only available in modern browsers
// https://caniuse.com/mdn-javascript_operators_await_top_level
const res = await getDataFromBackend();
console.log(res);

When you log the return value of this function, you’ll see in your developer tools the same information we previously could only see on the terminal, which means that we’re now able to use our data on the front end! Good job! 🎉

The console.log() of data inside getDataFromBackend()

Let’s now show this data inside our <div id="container"></div>, which will be very easy. We’ll start by doing a getElementById to get the proper element, and then we’ll create a function that will run getDataFromBackend() and will iterate over each object inside our data array and push this content to our HTML. Here’s my approach to it:

// Add data to HTML
const addData = async () => {
  const data = await getDataFromBackend();

  data.forEach((value) => {
    const div = document.createElement("div");
    div.classList.add("userContainer");
    div.innerHTML = `
        <h3>${value.name}</h3>
        <p>${value.role}</p>
    `;

    container.append(div);
  });
};

addData();

So, once again, our data variable (inside the addData function) is the same information that we could see logged (the array of objects) and we’re looping over it by creating a <div> with the class of userContainer, and inside it we have the name and role for each of our entries on the database.

If you now run your code, you should see something like what’s pictured below!

What you should see when rendering the data on your HTML

Writing Data to Our Notion Database

This would be a great stopping point for our experimentation with the Notion API, but we can do even more! Let’s now add new entries to our database by using the Create Page POST request (which can be found here) so that we have a fully functioning and working application using pretty much all the abilities of the Notion API.

So, our idea here will be to add a form on our front end that, when filled in and submitted, will push new data to our database, which will then show up in our front end!

Let’s start by adding a new function on our index.js called newEntryToDatabase. Considering the documentation, we now should do const response = await notion.pages.create(), and we should pass an object that matches the current database we’re working on. It will also have two arguments, name and role, which, for this project, would look like this:

exports.newEntryToDatabase = async function (name, role) {
  const response = await notion.pages.create({
    parent: {
      database_id: process.env.NOTION_API_DATABASE,
    },
    properties: {
      Name: {
        title: [
          {
            text: {
              content: name,
            },
          },
        ],
      },
      Role: {
        rich_text: [
          {
            text: {
              content: role,
            },
          },
        ],
      },
    },
  });

  return response;
};

Notice what we’re doing on this object. We’re basically doing the same thing that we were doing on getDatabase with our responseResults variable, by walking through each property until we get to the property we actually want to work with. Here, we’re using our arguments as values to the properties. If this looks confusing, it’s absolutely okay; review the linked documentation on this section to see more examples!

Now, jumping to our server.js, let’s not forget to import our new function with const newEntryToDatabase = moduleToFetch.newEntryToDatabase; at the top of the file. We’ll also do a POST request using app.post(). Here we also need a route (it will be /submit-form), and our callback function should get the name and role from the request (our filled-in form fields) and invoke newEntryToDatabase with these two arguments. We then finish our function with a redirect to our base route, / and we also end our request.

Our server.js file will also need a bit of code inside an app.use() function, which is the express.urlencoded. This is middleware for Express, so we can use the POST request, since we’re actually sending data:

const express = require("express");
const moduleToFetch = require("./index");
const getDatabase = moduleToFetch.getDatabase;
// importing our function
const newEntryToDatabase = moduleToFetch.newEntryToDatabase;
const port = 8000;

const app = express();

app.use(express.static("public"));
app.use(
  express.urlencoded({
    extended: true,
  })
);

app.get("/users", async (req, res) => {
  const users = await getDatabase();
  res.json(users);
});

// our newly added bit of code
app.post("/submit-form", async (req, res) => {
  const name = req.body.name;
  const role = req.body.role;
  await newEntryToDatabase(name, role);
  res.redirect("/");
  res.end();
});

app.listen(port, console.log(`Server started on ${port}`));

Our back end is now done, and we should work on our front-end code. At this point, you should restart your Express server so that it recognizes the changes.

To be fair, the only thing you need on your front-end code is a <form> in your HTML with method="POST" and action="/submit-form". This basically tells our code what type of form this should be, and also links it to a route (/submit-form), which we created to process requests.

So something like the following would be more than enough:

<form method="POST" action="/submit-form">
  <input type="text" name="name" placeholder="Insert user name" required />
  <input type="text" name="role" placeholder="Insert user role" required />
  <input type="submit" />
</form>

If we fill in the fields and submit our form and reload the page, we would see a new entry, and if we step into our Notion workspace, we’ll see the entry there. The functionality is complete. Well done! 🎉

But to improve our interface, the idea here is that we’ll have a button that, when clicked, will open a modal with the form (also with the possibility of closing it without filling it), so here’s my HTML:

<!-- The rest of the code above -->
<div id="wrapper">
  <div id="container"></div>
</div>

<div id="addUserFormContainer">
  <button id="closeFormButton">Close</button>
  <form method="POST" action="/submit-form" id="addUserForm">
    <h1 id="formTitle">Add a new user to your database</h1>
    <input
      type="text"
      name="name"
      placeholder="Insert user name"
      class="inputField"
      required
    />
    <input
      type="text"
      name="role"
      placeholder="Insert user role"
      class="inputField"
      required
    />
    <input type="submit" id="submitFormInput" />
  </form>
</div>

<button id="newUserButton">Add a new user</button>

<script type="module" src="./main.js"></script>
<!-- The rest of the code below -->

And here’s the CSS that should accompany it:

/* The rest of the code above */
#newUserButton {
  position: absolute;
  bottom: 10px;
  right: 10px;

  padding: 10px 20px;

  background-color: #ef4444;
  color: white;
  font-weight: bold;

  border: none;
  border-radius: 4px;
}

#addUserFormContainer {
  position: absolute;
  top: 0;
  left: 0;

  height: 100vh;
  width: 100vw;

  display: none;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  background: rgba(255, 255, 255, 0.4);
  backdrop-filter: blur(20px);
}

#closeFormButton {
  position: absolute;
  top: 10px;
  right: 10px;

  padding: 10px 20px;

  background-color: black;
  color: white;
  font-weight: bold;

  border: none;
  border-radius: 4px;
}

#formTitle {
  margin-bottom: 40px;
}

#addUserForm {
  padding: 50px 100px;
  width: 300px;

  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  background: white;

  box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
}

#addUserForm input {
  width: 100%;
  box-sizing: border-box;
}

.inputField {
  margin-bottom: 20px;
  padding: 10px 20px;

  border: 1px solid #b3b3b3;
  border-radius: 4px;
}

#submitFormInput {
  padding: 10px 20px;
  margin-bottom: none;

  background-color: #ef4444;
  color: white;
  font-weight: bold;

  border: 1px solid #ef4444;
  border-radius: 4px;
}

If you now visit your page, you’ll only see a red button with no real utility to it, so now we need to work on our JavaScript. Let’s therefore jump into our /public/main.js file!

Here, we’ll start by grabbing the #newUserButton, the #closeFormButton and the #addUserFormContainer:

const container = document.getElementById("container");
// the new variables
const openFormButton = document.getElementById("newUserButton");
const closeFormButton = document.getElementById("closeFormButton");
const addUserFormContainer = document.getElementById("addUserFormContainer");

Now on our openFormButton we’ll add a click event listener that will end up styling our addUserFormContainer with display: flex:

openFormButton.addEventListener("click", () => {
  addUserFormContainer.style.display = "flex";
});

Now if you click the Add a new user button, it will open the form.

To close our form modal, we just need remove this flex that we’re adding by pressing the closeFormButton, so it should look like this:

closeFormButton.addEventListener("click", () => {
  addUserFormContainer.style.display = "none";
});

And … we’re done! Now, when you enter a name and a role into the form, they’ll be added to your Notion database and will show up in the front end of the application.

We’ve just built a fully functioning website that gets a database, processes the data, displays it and also allows you to add to it! Isn’t that incredible?

Here’s a short video demo of the finished result.

Taking It Further

While this demo showcases some of the important uses of the Notion API, there’s still room for improvement in our app. For example, it would be advisable to implement some error handling, or a loading spinner showing when the app is communicating with Notion (and thus unresponsive). Also, instead of always querying the API to retrieve the data, you could quite easily turn this into a single-page application that queries the API once, then keeps the data we’re working with in state.

If you’d like help implementing any of this, or would like to showcase your solution, why not head over to SitePoint forums and let us know.

Conclusion

With this project, we ended up exploring almost the full functionality of the Notion API, and I think it’s pretty clear how amazing it can actually be!

I hope this post has given you a full view of the Notion API and inspired you to create more stuff with it!

If you want to quickly test this project, you can clone it from our GitHub repo.