Integrating Whoop Fitness Data into your portfolio with Next.js and Vercel

1/26/2024

Introduction

I recently acquired a Whoop and wanted to showcase my fitness data on my portfolio. In this post, I'll guide you through the steps to seamlessly integrate Whoop data into your portfolio.

Tutorial Outline

This tutorial will be divided into three main points:

  1. Discovering the Whoop API
  2. Presenting the strategy to fetch data from the Whoop API
  3. Setting up the repository and creating the codebase

Discovering the Whoop API Documentation

Working with the Whoop API involves navigating a standard OAuth system. To query the Whoop server, you need an access_token in the auth headers.

Each access token expires after 1 hour (3600 seconds). A refresh token is required to perform additional queries after the access token expires.

If the scopes include an offline value, the Whoop API response includes:

{
  "access_token": "the-value-of-the-new-access-token",
  "expires_in": 3600,
  "refresh_token": "the-value-of-the-new-refresh-token",
  "scope": "offline other-scopes-requested",
  "token_type": "bearer"
}

Create an Whoop application

Register your app on the Whoop developer dashboard and save the client_id, the client_secret they provide you. We will use it later.

Setting Up the Repository and Creating the Codebase

Create an .env file

# VERCEL
VERCEL_ACCESS_TOKEN=
VERCEL_PROJECT_ID=
VERCEL_WHOOP_REFRESH_TOKEN_ENV_VARIABLE_ID=

# WHOOP
WHOOP_CLIENT_ID=
WHOOP_CLIENT_SECRET=
WHOOP_TOKEN_URL=https://api.prod.whoop.com/oauth/oauth2/token
WHOOP_ACCESS_TOKEN=
WHOOP_REFRESH_TOKEN=

Create the Next.js page

Create a Next.js page, e.g., pages/sleep.tsx:

// pages/sleep.tsx

const SleepPage = ({ sleepData }) => {
  console.log({ sleepData })

  return <div>My Health page</div>
}

export const getStaticProps: GetStaticProps = async () => {
  const sleepData = await getSleepData()

  return {
    props: { sleepData }
  }
}

export default SleepPage

The next step will be to create the logic inside our fetching function: getSleepData.

Handle the Logic in getSleepData

Create a fetching function in libs/whoop.ts:

// libs/whoop.ts

export const getSleepData = async () => {}

The goal of this function will be multiple:

  1. request new tokens - access and refresh
  2. update env variables
  3. call the Whoop API
// libs/whoop.ts

export const getSleepData = async () => {
  const sleepData = await fetch(
    '<https://api.prod.whoop.com/developer/v1/activity/sleep>',
    {
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
    }
  )

  return sleepData.json()
}

With this configuration, if the access_token has expired, incoming requests will throw 401 Unauthorized errors, so we need to request fresh tokens.

// libs/whoop.js

const getRefreshTokens = async () => {
  const fetchStoredRefreshToken = await fetch(`https://api.vercel.com/v1/projects/${process.env.VERCEL_PROJECT_ID}/env/${process.env.VERCEL_WHOOP_REFRESH_TOKEN_ENV_VARIABLE_ID}`,
    {
      headers: {
        Authorization: `Bearer ${process.env.VERCEL_ACCESS_TOKEN}`,
      },
    }
  )

  const refreshToken = await fetchStoredRefreshToken.json()

  const getWhoopRefreshTokens = await fetch(`${process.env.WHOOP_TOKEN_URL}`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      grant_type: 'refresh_token',
      refresh_token: refreshToken.value,
      client_id: `${process.env.WHOOP_CLIENT_ID}`,
      client_secret: `${process.env.WHOOP_CLIENT_SECRET}`,
      scope: 'offline read:sleep',
    }),
  })

  const response = await getWhoopRefreshTokens.json()

  return response
}
// libs/whoop.js

export const getSleepData = async () => {
  const { access_token, refresh_token: refreshToken } = await getRefreshTokens()

  await fetch('/api/vercel', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ refreshToken }),
  })

  const sleepData = await fetch(
    '<https://api.prod.whoop.com/developer/v1/activity/sleep>',
    {
      headers: {
        Authorization: `Bearer ${access_token}`,
      },
    }
  )

  return sleepData.json()
}

Create the API route to update env variable

// api/vercel.js

export default async function handler(req, res) {
  const updateVercelEnvVariable = await fetch(`https://api.vercel.com/v9/projects/${process.env.VERCEL_PROJECT_ID}/env/${process.env.VERCEL_WHOOP_REFRESH_TOKEN_ENV_VARIABLE_ID}`, {
  method: 'PATCH',
        headers: {
          Authorization: `Bearer ${process.env.VERCEL_ACCESS_TOKEN}`,
        },
        body: JSON.stringify({
          value: req.body.refreshToken,
        }),
      }
    )

    await updateVercelEnvVariable.json()

    res.status(200)
}

Conclusion

This article provides a step-by-step guide to integrating Whoop fitness data into your portfolio using Next.js and Vercel. For the complete code, refer to the repository.

Happy coding!