Vibe Coding

Debugging Fitbit API Integration and Building a Fitness Data Table

TL;DR

  • Fixed broken Fitbit sync by correcting API endpoint paths for weight and heart rate data
  • Discovered the issue: endpoints were missing critical path segments (/body/log/ and /activities/)
  • Created a new API endpoint to fetch stored fitness data from the database
  • Built a simple data table on the heatmap page to display synced Fitbit metrics
  • All changes deployed and working in production

Today I spent time debugging and fixing the Fitbit integration on laverty.io, then built out the first visualization of that data.

Here’s what happened and what I learned.


The problem: sync was failing

After successfully connecting my Fitbit account via OAuth, clicking “Sync Fitbit Data” would fail with a generic error message on the frontend.

Looking at the production logs via wrangler pages deployment tail, I could see the real issue:

(error) Fitbit sync error: Error: Failed to fetch Fitbit weight data

The sync endpoint was throwing an error when trying to fetch weight data from Fitbit’s API.


Finding the root cause

I opened up the sync endpoint code at apps/web/src/pages/api/fitbit/sync.ts and found the problem immediately.

The code was constructing API URLs like this:

let url = `https://api.fitbit.com/1/user/-/${dataType}/date/${start}/${end}.json`;
if (dataType === 'sleep') {
  url = `https://api.fitbit.com/1.2/user/-/sleep/date/${start}/${end}.json`;
}

This meant:

  • Weight endpoint: /1/user/-/weight/date/{start}/{end}.json
  • Heart rate endpoint: /1/user/-/heartrate/date/{start}/{end}.json

Both were wrong.

I checked Fitbit’s official Web API documentation and found the correct endpoint formats:

  • Weight: /1/user/-/body/log/weight/date/{start}/{end}.json
  • Heart rate: /1/user/-/activities/heart/date/{start}/{end}.json

The weight endpoint was missing /body/log/, and the heart rate endpoint was missing /activities/ and using the wrong resource name entirely.


The fix

I replaced the conditional logic with a proper switch statement to make the endpoint construction explicit:

let url: string;

switch (dataType) {
  case 'weight':
    url = `https://api.fitbit.com/1/user/-/body/log/weight/date/${start}/${end}.json`;
    break;
  case 'heartrate':
    url = `https://api.fitbit.com/1/user/-/activities/heart/date/${start}/${end}.json`;
    break;
  case 'sleep':
    url = `https://api.fitbit.com/1.2/user/-/sleep/date/${start}/${end}.json`;
    break;
  default:
    throw new Error(`Unknown data type: ${dataType}`);
}

This made the code:

  • more explicit
  • easier to maintain
  • harder to break accidentally

After pushing the fix, syncing worked immediately. The endpoint successfully pulled 30 days of weight, heart rate, and sleep data from Fitbit and stored it in the D1 database.


Building the data table

With the sync working, I wanted to actually see the data.

The heatmap page at /fitcypher/heatmap was just a placeholder, so I decided to turn it into a simple data table.

Creating the API endpoint

First, I created a new endpoint at apps/web/src/pages/api/fitness/data.ts:

export const GET: APIRoute = async (context) => {
  // Check authentication
  const sessionCookie = context.cookies.get('__session')?.value;
  if (!sessionCookie) {
    return new Response(JSON.stringify({ error: 'Not authenticated' }), {
      status: 401,
      headers: { 'Content-Type': 'application/json' }
    });
  }

  // Get user ID from header
  const userId = context.request.headers.get('X-User-Id');
  if (!userId) {
    return new Response(JSON.stringify({
      error: 'User ID not provided'
    }), { status: 400 });
  }

  const db = context.locals.runtime.env.DB;

  // Fetch fitness data ordered by date descending
  const results = await db.prepare(
    `SELECT date, weight_kg, heart_rate_bpm, sleep_minutes, synced_at
     FROM fitness_data
     WHERE user_id = ?
     ORDER BY date DESC`
  ).bind(userId).all();

  return new Response(JSON.stringify({
    success: true,
    data: results.results || []
  }), { status: 200 });
};

This endpoint:

  • requires authentication via Clerk
  • takes the user ID from a request header
  • queries the D1 database for that user’s fitness data
  • returns it ordered by date (newest first)

Building the frontend table

Next, I updated the heatmap page to fetch and display this data.

The key parts:

Authentication using Clerk:

async function getClerkUserId(): Promise<string | null> {
  if (clerkUserId) return clerkUserId;

  // Wait for Clerk to load
  for (let i = 0; i < 50; i++) {
    const user = (window as any).Clerk?.user;
    if (user?.id) {
      clerkUserId = user.id;
      return clerkUserId;
    }
    await new Promise(resolve => setTimeout(resolve, 100));
  }
  return null;
}

This pattern waits for Clerk to initialize on the page, then retrieves the current user’s ID.

Fetching and displaying the data:

const userId = await getClerkUserId();

const response = await fetch('/api/fitness/data', {
  headers: { 'X-User-Id': userId }
});

const result = await response.json();
const data = result.data || [];

// Build table HTML dynamically
let tableHTML = `
  <table class="fitness-table">
    <thead>
      <tr>
        <th>Date</th>
        <th>Sleep (hours)</th>
        <th>Weight (kg)</th>
        <th>Heart Rate (bpm)</th>
      </tr>
    </thead>
    <tbody>
`;

data.forEach((row: any) => {
  const sleepHours = row.sleep_minutes ? (row.sleep_minutes / 60).toFixed(1) : '-';
  const weight = row.weight_kg ? row.weight_kg.toFixed(1) : '-';
  const heartRate = row.heart_rate_bpm || '-';

  tableHTML += `
    <tr>
      <td>${row.date}</td>
      <td>${sleepHours}</td>
      <td>${weight}</td>
      <td>${heartRate}</td>
    </tr>
  `;
});

tableHTML += `</tbody></table>`;
container.innerHTML = tableHTML;

The table:

  • converts sleep from minutes to hours (e.g., 450 minutes → 7.5 hours)
  • displays weight to one decimal place
  • shows heart rate as-is
  • handles missing values gracefully with ”-“

What’s working now

After deploying these changes:

✅ Fitbit sync pulls weight, heart rate, and sleep data correctly ✅ Data is stored in the D1 database with proper user association ✅ The heatmap page displays a clean table of all synced data ✅ Newest entries appear at the top ✅ Missing data points show as ”-” instead of breaking the UI


What this enables

Now that the data pipeline is working:

  • syncing happens reliably
  • data is stored in a queryable format
  • visualization is straightforward

From here, I can:

  • add charts and graphs
  • build trend analysis
  • create custom metrics
  • experiment with different visualizations

The table is intentionally simple — it’s a foundation, not a finished product.


Lessons from debugging

1. Check the API documentation first

I assumed the endpoint paths were correct, but they weren’t. A quick look at Fitbit’s docs would have saved debugging time.

2. Explicit is better than clever

The switch statement is longer than the original conditional logic, but it’s much clearer what each endpoint should be.

3. Logs are essential

Without wrangler pages deployment tail, I would have been guessing at what went wrong. Production logs showed the exact error immediately.

4. Test in production

Local development worked fine because I hadn’t tried syncing yet. The bug only appeared when hitting Fitbit’s real API in production.


Next steps

The fitness tracking system is now functional, but there’s room to expand:

  • add more Fitbit metrics (steps, active minutes, calories)
  • build actual heatmap visualizations (not just a table)
  • create weekly/monthly summaries
  • add goal tracking
  • implement data export

For now, though, the core integration works, and the data is accessible.

More updates to come.

Comments (...)

Loading comments...