AWESOME CODING

Using Notion API with JavaScript

October 08, 2022

Notion is an awesome organizational tool I have been using for a long time. It has so many capabilities that it slowly replaced many other tools I use daily. One of the ways that I use Notion is to create content for my personal website.

My personal website is built as a static website. This means that I have to provide all the information I want inside the website before deploying it on the web. Once it is deployed, it doesn’t change. This means I must redeploy the website anytime I want to update it.

This might sound cumbersome, but it isn’t. Building, updating and deploying a website means a couple more steps are involved, but it doesn’t take much more time. Additionally, having a static website has some benefits in terms of performance, cost-effectiveness and resilience. A static website is displayed quickly from anywhere worldwide, can be hosted freely and wouldn’t crash under heavy traffic. It happens to be a great architectural decision for certain kinds of websites.

There is a page on my website listing the books I like. Maintaining this kind of long and dynamic list using HTML can be cumbersome. You would probably need a visual editor accessible from anywhere to edit that content easily.

That’s why I have a database in Notion that I could easily populate with book information and import into my website. You can see what that database looks like here. Before discovering the Notion API, I used to export this database as a CSV and parse that CSV to populate the books list page. However, this process included too many steps, so I avoided doing it frequently.

However, Notion API makes this process super simple. In this post, I will show how I interact with the Notion API using JavaScript to populate my application with data from my Notion account.

Integrating with the Notion API

You can find the repository that includes the code from this post in my github repo.

This post assumes that you have a Notion account. Go to the https://www.notion.so/my-integrations page to create an API integration with Notion. An API integration essentially creates a way for you programmatically interact with a resource on Notion. That resource could be a page or a database. In this example, we will look into interacting with a database, but I will also have another post showing how to interact with pages.

We will create an internal integration in our workspace using read content capability.

Create an internal integration by giving it a name and some access capabilities. At a minimum, we need a read-content capability. Since we will be creating an API integration for our own usage, the capabilities we give to this app can be as generous as you would like. However, if we were to create a public integration, we would want to be more careful when deciding which access capabilities we might need. Too many and unneeded capabilities might make our users uneasy.

For this project, I created an integration called Book Reviews, but you can call yours whatever you want.

Create the integration and take note of the Internal Integration Token that is generated. This is a secret token that will allow us to control this integration. Now that we created the integration, go to a page in Notion where you would like to have this integration functional. For the sake of this example, choose a page that contains a full-page database. Then, click Add Connections to connect the integration to the page. If you don’t have a page available, you can duplicate mine: https://cuboid-hoof-835.notion.site/7c579be494fc4f87bc827c6653f52dfd?v=b03c21ff315740c8a951819476a82526

We need to note the ID of this resource (page/database), so we can refer to it in our code. You can find the ID in the URL. Here is what the URL looks like in my case https://www.notion.so/7c579be494fc4f87bc827c6653f52dfd?v=b03c21ff315740c8a951819476a82526. The ID is the part that is before ? and after the domain name, which is 7c579be494fc4f87bc827c6653f52dfd.

Fetching Data from Notion API

We can connect to the Notion API just like how you would connect to any API. For example, we can use the command line curl tool. However, Notion has a JavaScript client library that makes interacting with this library much easier. We will be using it in our example. I will be assuming you have Node.js and npm installed on your system.

Let’s create a new project by creating a folder and then creating a package.json file inside that folder.

mkdir book-reviews

We will run npm init -y to initialize an empty package.json inside this folder.

npm init -y

Now that we have a package.json file, we can start installing the packages that we will be using in this project.

npm install --save @notionhq/client@^2.2.1
npm install --save dotenv@^16.0.3

@notionhq/client is the library that will help us use the Notion API easier. dotenv is a library that makes it easier to use the secrets we store in environment variables. What secrets, you might ask. Remember the Internal Integration Token generated when we first created our integration? That will be the secret key we will use to communicate with the API. Let’s create a file called .env to store that secret and the database id.

NOTION_KEY=<Paste Your Internal Integration Token here>
NOTION_DATABASE_ID=<Pase your Notion Database ID here>

.env files are a way to manage secrets in application development. They are not committed to source control or anywhere that is public. To prevent it from going to git-based source control, you can ignore it using a .gitignore file alongside other things you might want to ignore, like node_modules.

.env
node_modules

We can now start writing our JavaScript logic to fetch the book list data from Notion! Create a file called index.js.

const dotenv = require('dotenv');

dotenv.config()

console.log(process.env.NOTION_KEY)
console.log(process.env.NOTION_DATABASE_ID)

Here we are importing the dotenv library and running the config method on it. This should load the variables stored inside the .env file into the environment. We are using console.log to print those variables to the screen to ensure they are working as expected.

We now have the pieces we need to start interacting with the Notion API and the database.

const Client = require('@notionhq/client').Client
const dotenv = require('dotenv')

dotenv.config()

const NOTION_CLIENT = new Client({ auth: process.env.NOTION_KEY })
const DATABASE_ID = process.env.NOTION_DATABASE_ID

async function getDatabaseData(client, databaseId) {
  const response = await client.databases.query({
    database_id: databaseId,
  })

  console.log(response)
}

getDatabaseData(NOTION_CLIENT, DATABASE_ID)

Here I am creating two constant variables used throughout the file: NOTION and DATABASE_ID.

We are also creating an async function called getDatabaseData that asynchronously connects to the given database_id using the Notion Client we instantiated. It takes the Notion Client library and the database id as arguments. It fetches the results and logs them to the screen.

async function getDatabaseData(client, databaseId) {
  const response = await client.databases.query({
    database_id: databaseId,
  })

  console.log(response)
}

Including some error handling is a good idea since many things can go wrong when working with a remote API. We will handle the errors using a try catch statement.

async function getDatabaseData(client, databaseId) {
  try {
    const response = await client.databases.query({
      database_id: databaseId,
    })

    console.log(response)
  } catch (error) {
    console.error(error)
  }
}

The database you are working with will be different than mine, but you might notice these values at the end of the data structure you receive. Note the has_more property.

next_cursor: '54bf7ed3-b876-45cc-b3ad-01eb8df0cb56',
has_more: true,
type: 'page',
page: {}

has_more being true means that the response you are receiving from the API is paginated. There is more data available than what we are currently receiving. The rest of the data is on the following pages. The Notion API returns only 100 results per query by default. The query results are contained inside response.results array. We will save the query results inside an array called results and log the length of that array (which will be logging a number that is 100 or less).

async function getDatabaseData(client, databaseId) {
  try {
    let results = []

    const response = await client.databases.query({
      database_id: databaseId,
    })

    results = [...results, ...response.results]
    console.log(results.length)
  } catch (error) {
    console.error(error)
  }
}

We will also update our code to get the results contained in the following pages.

async function getDatabaseData(client, databaseId) {
  try {
    let results = []

    const response = await client.databases.query({
      database_id: databaseId,
    })
    results = [...results, ...response.results]

    // while loop variables
    let hasMore = response.has_more
    let nextCursor = response.next_cursor

    // keep fetching while there are more results
    while (hasMore) {
      const response = await client.databases.query({
        database_id: databaseId,
        start_cursor: nextCursor,
      })
      results = [...results, ...response.results]
      hasMore = response.has_more
      nextCursor = response.next_cursor
    }

    console.log(results.length)
  } catch (error) {
    console.error(error)
  }
}

To fetch the next page, we need to provide a start_cursor value to the database query. start_cursor indicates the location in the list where we should be fetching the results. We are making it so that the start_cursor for the next fetch would equal the next_cursor of the current fetch results.

const response = await client.databases.query({
  database_id: databaseId,
  start_cursor: nextCursor,
})

We are doing this inside a while loop that will run until has_more is equal to false.

// while loop variables
let hasMore = response.has_more
let nextCursor = response.next_cursor

// keep fetching while there are more results
while (hasMore) {
  const response = await client.databases.query({
    database_id: databaseId,
    start_cursor: nextCursor,
  })
  results = [...results, ...response.results]
  hasMore = response.has_more
  nextCursor = response.next_cursor
}

With this logic in place, we should be able to fetch all the database data from Notion. Next, let’s format this data to use for our purposes.

Formatting the Data From Notion API

We will update the getDatabaseData function to return the results array instead of just logging it. We will use this function inside another function called main.

const Client = require('@notionhq/client').Client
const dotenv = require('dotenv')

dotenv.config()

const NOTION_CLIENT = new Client({ auth: process.env.NOTION_KEY })
const DATABASE_ID = process.env.NOTION_DATABASE_ID

async function getDatabaseData(client, databaseId) {
  try {
    let results = []

    const response = await client.databases.query({
      database_id: databaseId,
    })
    results = [...results, ...response.results]

    // while loop variables
    let hasMore = response.has_more
    let nextCursor = response.next_cursor

    // keep fetching while there are more results
    while (hasMore) {
      const response = await client.databases.query({
        database_id: databaseId,
        start_cursor: nextCursor,
      })
      results = [...results, ...response.results]
      hasMore = response.has_more
      nextCursor = response.next_cursor
    }

    return results
  } catch (error) {
    console.error(error)
  }
}

async function main() {
  const data = await getDatabaseData(NOTION_CLIENT, DATABASE_ID)

  console.log(data)
}

main()

We have simply added a return results statement inside the getDatabaseData function. We have additionally created the main function where we call the getDatabaseData function.

async function main() {
  const data = await getDatabaseData(NOTION_CLIENT, DATABASE_ID)

  console.log(data)
}

If we look at the received data, we will notice that the properties of the data we are most likely interested in are under the properties key.

properties: {
 review: [Object],
  url: [Object],
  quote: [Object],
  rating: [Object],
  priority: [Object],
  title: [Object]
},

These properties correspond to the columns of our Notion database. Let’s console.log them using JSON.stringify to see what they look like. Currently, we can only see that they are JavaScript objects ([Object]). Using JSON.stringify would help us investigate the internals of this structure.

async function main() {
  const data = await getDatabaseData(NOTION_CLIENT, DATABASE_ID)
  const firstDataItem = data[0]

  console.log(JSON.stringify(firstDataItem, null, 2))
}

You will notice that we are getting objects with many properties representing the data stored inside our database columns. Each piece of data contains meta information like id, type and rich_text. The rich_text property is particularly interesting, representing a text as an array of objects. The array might have more than one element if you use a different kind of styling for the words inside a text like bold, italic or code. My database entries are just using plain text, so I will not worry about this.

The only thing left to do for us here is to format the received Notion API data into something easier to consume. I will create a function called normalizeDataItem that would format a given data item.

function normalizeDataItem(item) {
  const { url, review, quote, rating, priority, title } = item.properties

  return {
    url: url.url,
    review: review.rich_text[0]?.plain_text ?? '',
    quote: quote.rich_text[0]?.plain_text ?? '',
    rating: rating.number,
    priority: priority.number,
    title: title.title[0]?.plain_text ?? '',
  }
}

We can run our results through this normalizeDataItem function to get a more usable data structure.

async function main() {
  const data = await getDatabaseData(NOTION_CLIENT, DATABASE_ID)
  const normalizedData = data.map((item) => normalizeDataItem(item))

  console.log(normalizedData)
}

We are pretty much done! It is probably not super fruitful to just console.log results. Let’s update our code to write the results into a JSON file. We will first import two built-in node libraries called path and fs at the top of our file.

const fs = require('fs')
const path = require('path')

We will then update our main function to write the returned results to a JSON file on disk.

async function main() {
  const data = await getDatabaseData(NOTION_CLIENT, DATABASE_ID)
  const normalizedData = data.map((item) => normalizeDataItem(item))

  const outputFilePath = path.join(__dirname, 'data.json')
  fs.writeFileSync(outputFilePath, JSON.stringify(normalizedData, null, 2))
}

And that’s pretty much it! You can now do whatever you want with this JSON file. In my case, I feed it into my Next.js app in build time to populate the website with the content from the data.

I hope you enjoyed this walkthrough. Formatting the page data from Notion is a bit more tricky. I will explore this in a separate blog post. Meanwhile, feel free to follow me up at: https://twitter.com/inspiratory and https://www.linkedin.com/in/enginarslan/.

Follow me on Social Media

Built by Engin