How to Build a Custom Dashboard Using the Pipedrive REST API

How to Build a Custom Dashboard Using the Pipedrive REST API?

The Problem

Pipedrive’s built-in reports lack flexibility for complex sales analytics. Your leadership needs custom metrics—win rate by lead source, average deal cycle by product line, rep performance with revenue forecasts—but Pipedrive’s interface forces you to export CSV files, manually manipulate data in spreadsheets, and create static charts that become outdated within hours. Sales managers waste time clicking through multiple screens to gather insights that should be visible at a glance. You need a live dashboard that pulls real-time data from Pipedrive’s API, calculates custom KPIs, and visualizes trends in a single view. However, Pipedrive’s API has pagination limits, rate throttling, field ID mapping complexities, and nested response structures that make data extraction challenging. You must handle authentication, parse JSON responses with varying schemas, aggregate data across multiple endpoints (deals, activities, users, organizations), and refresh metrics without hitting rate limits. This tutorial provides production-ready code for a custom dashboard that fetches Pipedrive data, calculates sales metrics, and displays real-time visualizations.

Tech Stack & Prerequisites

  • Node.js v18+ with npm
  • Express.js 4.18+ for backend server
  • React 18+ with Vite for frontend dashboard
  • Pipedrive Account with admin or API access
  • Pipedrive API Token (generate in Personal Preferences → API)
  • axios 1.6+ for HTTP requests
  • dotenv for environment variables
  • recharts 2.9+ for React charts
  • date-fns 3.0+ for date manipulation
  • tailwindcss 3.4+ for styling (optional)

Required Pipedrive Setup:

  • Active Pipedrive account (trial or paid)
  • API token with read permissions
  • Deal stages configured in pipeline
  • At least some historical deal data for testing

Optional:

  • PostgreSQL or MongoDB for caching API responses
  • Redis for rate limit tracking

Step-by-Step Implementation

Step 1: Setup

Initialize the project with backend and frontend:

bash
mkdir pipedrive-dashboard
cd pipedrive-dashboard

# Backend setup
mkdir backend
cd backend
npm init -y
npm install express axios dotenv cors node-cache
npm install --save-dev nodemon

# Frontend setup
cd ..
npm create vite@latest frontend -- --template react
cd frontend
npm install
npm install axios recharts date-fns
cd ..

Create backend structure:

bash
cd backend
mkdir src api utils
touch src/server.js api/pipedrive.js utils/cache.js .env .gitignore
```

Your structure should be:
```
pipedrive-dashboard/
├── backend/
│   ├── src/
│   │   └── server.js
│   ├── api/
│   │   └── pipedrive.js
│   ├── utils/
│   │   └── cache.js
│   ├── .env
│   ├── .gitignore
│   └── package.json
└── frontend/
    ├── src/
    │   ├── App.jsx
    │   ├── components/
    │   │   ├── Dashboard.jsx
    │   │   ├── MetricsCard.jsx
    │   │   └── DealsChart.jsx
    │   └── services/
    │       └── api.js
    └── package.json

backend/package.json — Add scripts:

json
{
  "name": "pipedrive-dashboard-backend",
  "version": "1.0.0",
  "type": "module",
  "scripts": {
    "start": "node src/server.js",
    "dev": "nodemon src/server.js"
  },
  "dependencies": {
    "axios": "^1.6.2",
    "cors": "^2.8.5",
    "dotenv": "^16.3.1",
    "express": "^4.18.2",
    "node-cache": "^5.1.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.1"
  }
}

backend/.gitignore:

bash
echo "node_modules/
.env
*.log" > .gitignore

Step 2: Configuration

backend/.env — Store Pipedrive API credentials:

env
# Pipedrive Configuration
PIPEDRIVE_API_TOKEN=your_api_token_here
PIPEDRIVE_COMPANY_DOMAIN=yourcompany
PIPEDRIVE_API_BASE_URL=https://api.pipedrive.com/v1

# Server Configuration
PORT=5000
NODE_ENV=development
FRONTEND_URL=http://localhost:5173

# Cache settings (in seconds)
CACHE_TTL_DEALS=300
CACHE_TTL_ACTIVITIES=300
CACHE_TTL_USERS=3600

How to get Pipedrive API token:

  1. Log into Pipedrive
  2. Click your profile icon → Personal preferences
  3. Go to API tab
  4. Click Generate new token
  5. Copy the token (starts with letters and numbers)
  6. Your company domain is from your URL: https://yourcompany.pipedrive.com

utils/cache.js — Simple in-memory cache to avoid rate limits:

javascript
import NodeCache from 'node-cache';
import dotenv from 'dotenv';

dotenv.config();

// Create cache instances with different TTLs
const dealsCache = new NodeCache({ 
  stdTTL: parseInt(process.env.CACHE_TTL_DEALS) || 300 
});

const activitiesCache = new NodeCache({ 
  stdTTL: parseInt(process.env.CACHE_TTL_ACTIVITIES) || 300 
});

const usersCache = new NodeCache({ 
  stdTTL: parseInt(process.env.CACHE_TTL_USERS) || 3600 
});

export function getCachedData(key, cacheType = 'deals') {
  const cache = getCacheInstance(cacheType);
  return cache.get(key);
}

export function setCachedData(key, data, cacheType = 'deals') {
  const cache = getCacheInstance(cacheType);
  cache.set(key, data);
}

export function clearCache(cacheType = 'all') {
  if (cacheType === 'all') {
    dealsCache.flushAll();
    activitiesCache.flushAll();
    usersCache.flushAll();
  } else {
    getCacheInstance(cacheType).flushAll();
  }
}

function getCacheInstance(type) {
  switch (type) {
    case 'activities':
      return activitiesCache;
    case 'users':
      return usersCache;
    default:
      return dealsCache;
  }
}

export default { getCachedData, setCachedData, clearCache };

api/pipedrive.js — Pipedrive API client with pagination:

javascript
import axios from 'axios';
import dotenv from 'dotenv';
import { getCachedData, setCachedData } from '../utils/cache.js';

dotenv.config();

const PIPEDRIVE_API_TOKEN = process.env.PIPEDRIVE_API_TOKEN;
const BASE_URL = process.env.PIPEDRIVE_API_BASE_URL;

// Create axios instance with default config
const pipedriveClient = axios.create({
  baseURL: BASE_URL,
  params: {
    api_token: PIPEDRIVE_API_TOKEN,
  },
});

// Generic function to fetch all pages from Pipedrive
async function fetchAllPages(endpoint, params = {}) {
  const allData = [];
  let start = 0;
  const limit = 100; // Max items per page
  let hasMore = true;

  while (hasMore) {
    try {
      const response = await pipedriveClient.get(endpoint, {
        params: {
          ...params,
          start,
          limit,
        },
      });

      if (response.data.success && response.data.data) {
        allData.push(...response.data.data);
        
        // Check if there are more pages
        const pagination = response.data.additional_data?.pagination;
        hasMore = pagination?.more_items_in_collection || false;
        start = pagination?.next_start || 0;
      } else {
        hasMore = false;
      }
    } catch (error) {
      console.error(`Error fetching ${endpoint}:`, error.message);
      hasMore = false;
    }
  }

  return allData;
}

// Fetch all deals with optional filters
export async function getAllDeals(status = 'all_not_deleted') {
  const cacheKey = `deals_${status}`;
  const cached = getCachedData(cacheKey);
  
  if (cached) {
    console.log('✓ Returning cached deals');
    return cached;
  }

  console.log('Fetching deals from Pipedrive API...');
  
  const deals = await fetchAllPages('/deals', { status });
  
  setCachedData(cacheKey, deals);
  console.log(`✓ Fetched ${deals.length} deals`);
  
  return deals;
}

// Fetch deals won within date range
export async function getDealsWon(startDate, endDate) {
  const cacheKey = `deals_won_${startDate}_${endDate}`;
  const cached = getCachedData(cacheKey);
  
  if (cached) {
    return cached;
  }

  const deals = await fetchAllPages('/deals', {
    status: 'won',
    start_date: startDate,
    end_date: endDate,
  });
  
  setCachedData(cacheKey, deals);
  return deals;
}

// Fetch all users (sales reps)
export async function getAllUsers() {
  const cacheKey = 'all_users';
  const cached = getCachedData(cacheKey, 'users');
  
  if (cached) {
    return cached;
  }

  console.log('Fetching users from Pipedrive API...');
  
  try {
    const response = await pipedriveClient.get('/users');
    const users = response.data.success ? response.data.data : [];
    
    setCachedData(cacheKey, users, 'users');
    console.log(`✓ Fetched ${users.length} users`);
    
    return users;
  } catch (error) {
    console.error('Error fetching users:', error.message);
    return [];
  }
}

// Fetch all pipeline stages
export async function getPipelineStages() {
  const cacheKey = 'pipeline_stages';
  const cached = getCachedData(cacheKey, 'users');
  
  if (cached) {
    return cached;
  }

  try {
    const response = await pipedriveClient.get('/stages');
    const stages = response.data.success ? response.data.data : [];
    
    setCachedData(cacheKey, stages, 'users');
    return stages;
  } catch (error) {
    console.error('Error fetching stages:', error.message);
    return [];
  }
}

// Fetch activities (calls, meetings, emails)
export async function getActivities(startDate, endDate, type = null) {
  const cacheKey = `activities_${startDate}_${endDate}_${type}`;
  const cached = getCachedData(cacheKey, 'activities');
  
  if (cached) {
    return cached;
  }

  const params = {
    start_date: startDate,
    end_date: endDate,
  };
  
  if (type) {
    params.type = type;
  }

  const activities = await fetchAllPages('/activities', params);
  
  setCachedData(cacheKey, activities, 'activities');
  return activities;
}

// Calculate dashboard metrics
export async function calculateDashboardMetrics() {
  const cacheKey = 'dashboard_metrics';
  const cached = getCachedData(cacheKey);
  
  if (cached) {
    console.log('✓ Returning cached metrics');
    return cached;
  }

  console.log('Calculating dashboard metrics...');

  // Fetch required data
  const [allDeals, users, stages] = await Promise.all([
    getAllDeals(),
    getAllUsers(),
    getPipelineStages(),
  ]);

  // Calculate metrics
  const wonDeals = allDeals.filter(d => d.status === 'won');
  const lostDeals = allDeals.filter(d => d.status === 'lost');
  const openDeals = allDeals.filter(d => d.status === 'open');

  const totalRevenue = wonDeals.reduce((sum, deal) => sum + (deal.value || 0), 0);
  const averageDealValue = wonDeals.length > 0 ? totalRevenue / wonDeals.length : 0;
  
  const totalDeals = wonDeals.length + lostDeals.length;
  const winRate = totalDeals > 0 ? (wonDeals.length / totalDeals) * 100 : 0;

  // Calculate pipeline value (open deals)
  const pipelineValue = openDeals.reduce((sum, deal) => sum + (deal.value || 0), 0);

  // Deals by stage
  const dealsByStage = stages.map(stage => {
    const stageDeals = openDeals.filter(d => d.stage_id === stage.id);
    return {
      name: stage.name,
      count: stageDeals.length,
      value: stageDeals.reduce((sum, d) => sum + (d.value || 0), 0),
    };
  });

  // Deals by owner (sales rep performance)
  const dealsByOwner = users.map(user => {
    const userWonDeals = wonDeals.filter(d => d.user_id?.id === user.id);
    const userOpenDeals = openDeals.filter(d => d.user_id?.id === user.id);
    
    return {
      name: user.name,
      wonDeals: userWonDeals.length,
      revenue: userWonDeals.reduce((sum, d) => sum + (d.value || 0), 0),
      openDeals: userOpenDeals.length,
      pipelineValue: userOpenDeals.reduce((sum, d) => sum + (d.value || 0), 0),
    };
  }).filter(user => user.wonDeals > 0 || user.openDeals > 0);

  // Monthly revenue trend (last 6 months)
  const monthlyRevenue = calculateMonthlyRevenue(wonDeals);

  const metrics = {
    overview: {
      totalRevenue,
      averageDealValue,
      winRate: winRate.toFixed(1),
      pipelineValue,
      wonDealsCount: wonDeals.length,
      openDealsCount: openDeals.length,
    },
    dealsByStage,
    dealsByOwner,
    monthlyRevenue,
    lastUpdated: new Date().toISOString(),
  };

  setCachedData(cacheKey, metrics);
  console.log('✓ Metrics calculated');

  return metrics;
}

// Helper: Calculate monthly revenue for last 6 months
function calculateMonthlyRevenue(wonDeals) {
  const now = new Date();
  const months = [];
  
  // Generate last 6 months
  for (let i = 5; i >= 0; i--) {
    const date = new Date(now.getFullYear(), now.getMonth() - i, 1);
    const monthKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`;
    months.push({
      month: date.toLocaleDateString('en-US', { month: 'short', year: 'numeric' }),
      monthKey,
      revenue: 0,
      deals: 0,
    });
  }

  // Aggregate deals by month
  wonDeals.forEach(deal => {
    if (!deal.won_time) return;
    
    const wonDate = new Date(deal.won_time);
    const monthKey = `${wonDate.getFullYear()}-${String(wonDate.getMonth() + 1).padStart(2, '0')}`;
    
    const monthData = months.find(m => m.monthKey === monthKey);
    if (monthData) {
      monthData.revenue += deal.value || 0;
      monthData.deals += 1;
    }
  });

  return months;
}

export default {
  getAllDeals,
  getDealsWon,
  getAllUsers,
  getPipelineStages,
  getActivities,
  calculateDashboardMetrics,
};

Step 3: Core Logic

src/server.js — Express API server:

javascript
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import { calculateDashboardMetrics, getAllDeals, getAllUsers } from '../api/pipedrive.js';
import { clearCache } from '../utils/cache.js';

dotenv.config();

const app = express();
const PORT = process.env.PORT || 5000;

// Middleware
app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:5173',
}));
app.use(express.json());

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});

// GET /api/dashboard - Main dashboard metrics
app.get('/api/dashboard', async (req, res) => {
  try {
    console.log('\n📊 Dashboard metrics requested');
    
    const metrics = await calculateDashboardMetrics();
    
    res.json({
      success: true,
      data: metrics,
    });
  } catch (error) {
    console.error('Error fetching dashboard:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// GET /api/deals - All deals with optional filters
app.get('/api/deals', async (req, res) => {
  try {
    const { status = 'all_not_deleted' } = req.query;
    
    const deals = await getAllDeals(status);
    
    res.json({
      success: true,
      count: deals.length,
      data: deals,
    });
  } catch (error) {
    console.error('Error fetching deals:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// GET /api/users - All users/sales reps
app.get('/api/users', async (req, res) => {
  try {
    const users = await getAllUsers();
    
    res.json({
      success: true,
      count: users.length,
      data: users,
    });
  } catch (error) {
    console.error('Error fetching users:', error);
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// POST /api/cache/clear - Clear cache manually
app.post('/api/cache/clear', (req, res) => {
  try {
    const { type = 'all' } = req.body;
    
    clearCache(type);
    
    res.json({
      success: true,
      message: `Cache cleared: ${type}`,
    });
  } catch (error) {
    res.status(500).json({
      success: false,
      error: error.message,
    });
  }
});

// Start server
app.listen(PORT, () => {
  console.log(`\n🚀 Pipedrive Dashboard API running on http://localhost:${PORT}`);
  console.log(`\nAPI Endpoints:`);
  console.log(`  GET  /api/dashboard - Dashboard metrics`);
  console.log(`  GET  /api/deals - All deals`);
  console.log(`  GET  /api/users - All users`);
  console.log(`  POST /api/cache/clear - Clear cache\n`);
});

frontend/src/services/api.js — Frontend API client:

javascript
import axios from 'axios';

const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000';

const apiClient = axios.create({
  baseURL: API_BASE_URL,
  headers: {
    'Content-Type': 'application/json',
  },
});

export async function fetchDashboardMetrics() {
  const response = await apiClient.get('/api/dashboard');
  return response.data;
}

export async function fetchDeals(status = 'all_not_deleted') {
  const response = await apiClient.get('/api/deals', {
    params: { status },
  });
  return response.data;
}

export async function clearCache(type = 'all') {
  const response = await apiClient.post('/api/cache/clear', { type });
  return response.data;
}

export default {
  fetchDashboardMetrics,
  fetchDeals,
  clearCache,
};

frontend/src/components/MetricsCard.jsx — Reusable metric display:

jsx
export default function MetricsCard({ title, value, subtitle, icon }) {
  return (
    <div className="bg-white rounded-lg shadow p-6">
      <div className="flex items-center justify-between">
        <div>
          <p className="text-sm font-medium text-gray-600">{title}</p>
          <p className="text-3xl font-bold text-gray-900 mt-2">{value}</p>
          {subtitle && (
            <p className="text-sm text-gray-500 mt-1">{subtitle}</p>
          )}
        </div>
        {icon && (
          <div className="text-4xl text-blue-500">{icon}</div>
        )}
      </div>
    </div>
  );
}

frontend/src/components/DealsChart.jsx — Revenue chart component:

jsx
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';

export default function DealsChart({ data, title }) {
  const formatCurrency = (value) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
      minimumFractionDigits: 0,
    }).format(value);
  };

  return (
    <div className="bg-white rounded-lg shadow p-6">
      <h3 className="text-lg font-semibold mb-4">{title}</h3>
      <ResponsiveContainer width="100%" height={300}>
        <LineChart data={data}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="month" />
          <YAxis tickFormatter={formatCurrency} />
          <Tooltip formatter={(value) => formatCurrency(value)} />
          <Line 
            type="monotone" 
            dataKey="revenue" 
            stroke="#3b82f6" 
            strokeWidth={2}
            dot={{ fill: '#3b82f6' }}
          />
        </LineChart>
      </ResponsiveContainer>
    </div>
  );
}

frontend/src/components/Dashboard.jsx — Main dashboard component:

jsx
import { useState, useEffect } from 'react';
import { fetchDashboardMetrics, clearCache } from '../services/api';
import MetricsCard from './MetricsCard';
import DealsChart from './DealsChart';

export default function Dashboard() {
  const [metrics, setMetrics] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [refreshing, setRefreshing] = useState(false);

  useEffect(() => {
    loadDashboard();
  }, []);

  async function loadDashboard() {
    try {
      setLoading(true);
      setError(null);
      
      const response = await fetchDashboardMetrics();
      
      if (response.success) {
        setMetrics(response.data);
      } else {
        setError('Failed to load dashboard');
      }
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  }

  async function handleRefresh() {
    try {
      setRefreshing(true);
      await clearCache('all');
      await loadDashboard();
    } catch (err) {
      setError(err.message);
    } finally {
      setRefreshing(false);
    }
  }

  const formatCurrency = (value) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
      minimumFractionDigits: 0,
    }).format(value);
  };

  if (loading) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-xl">Loading dashboard...</div>
      </div>
    );
  }

  if (error) {
    return (
      <div className="flex items-center justify-center min-h-screen">
        <div className="text-red-600">Error: {error}</div>
      </div>
    );
  }

  if (!metrics) return null;

  const { overview, dealsByStage, dealsByOwner, monthlyRevenue, lastUpdated } = metrics;

  return (
    <div className="min-h-screen bg-gray-100 p-8">
      <div className="max-w-7xl mx-auto">
        {/* Header */}
        <div className="flex justify-between items-center mb-8">
          <div>
            <h1 className="text-3xl font-bold text-gray-900">Sales Dashboard</h1>
            <p className="text-sm text-gray-500 mt-1">
              Last updated: {new Date(lastUpdated).toLocaleString()}
            </p>
          </div>
          <button
            onClick={handleRefresh}
            disabled={refreshing}
            className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
          >
            {refreshing ? 'Refreshing...' : '🔄 Refresh Data'}
          </button>
        </div>

        {/* Key Metrics */}
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
          <MetricsCard
            title="Total Revenue"
            value={formatCurrency(overview.totalRevenue)}
            subtitle={`${overview.wonDealsCount} deals won`}
            icon="💰"
          />
          <MetricsCard
            title="Win Rate"
            value={`${overview.winRate}%`}
            subtitle="Closed deals"
            icon="🎯"
          />
          <MetricsCard
            title="Avg Deal Value"
            value={formatCurrency(overview.averageDealValue)}
            subtitle="Per won deal"
            icon="📊"
          />
          <MetricsCard
            title="Pipeline Value"
            value={formatCurrency(overview.pipelineValue)}
            subtitle={`${overview.openDealsCount} open deals`}
            icon="🔄"
          />
        </div>

        {/* Monthly Revenue Chart */}
        <div className="mb-8">
          <DealsChart 
            data={monthlyRevenue} 
            title="Revenue Trend (Last 6 Months)" 
          />
        </div>

        {/* Deals by Stage */}
        <div className="bg-white rounded-lg shadow p-6 mb-8">
          <h3 className="text-lg font-semibold mb-4">Pipeline by Stage</h3>
          <div className="space-y-3">
            {dealsByStage.map((stage, index) => (
              <div key={index} className="flex items-center justify-between">
                <div className="flex-1">
                  <div className="flex justify-between mb-1">
                    <span className="font-medium">{stage.name}</span>
                    <span className="text-gray-600">
                      {stage.count} deals • {formatCurrency(stage.value)}
                    </span>
                  </div>
                  <div className="w-full bg-gray-200 rounded-full h-2">
                    <div
                      className="bg-blue-600 h-2 rounded-full"
                      style={{
                        width: `${(stage.value / overview.pipelineValue) * 100}%`,
                      }}
                    />
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>

        {/* Sales Rep Performance */}
        <div className="bg-white rounded-lg shadow p-6">
          <h3 className="text-lg font-semibold mb-4">Sales Rep Performance</h3>
          <div className="overflow-x-auto">
            <table className="min-w-full">
              <thead>
                <tr className="border-b">
                  <th className="text-left py-2">Rep</th>
                  <th className="text-right py-2">Won Deals</th>
                  <th className="text-right py-2">Revenue</th>
                  <th className="text-right py-2">Open Deals</th>
                  <th className="text-right py-2">Pipeline Value</th>
                </tr>
              </thead>
              <tbody>
                {dealsByOwner
                  .sort((a, b) => b.revenue - a.revenue)
                  .map((rep, index) => (
                    <tr key={index} className="border-b hover:bg-gray-50">
                      <td className="py-3">{rep.name}</td>
                      <td className="text-right">{rep.wonDeals}</td>
                      <td className="text-right font-medium">
                        {formatCurrency(rep.revenue)}
                      </td>
                      <td className="text-right">{rep.openDeals}</td>
                      <td className="text-right">
                        {formatCurrency(rep.pipelineValue)}
                      </td>
                    </tr>
                  ))}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  );
}

frontend/src/App.jsx — Main app entry:

jsx
import Dashboard from './components/Dashboard';
import './App.css';

function App() {
  return (
    <div className="App">
      <Dashboard />
    </div>
  );
}

export default App;

frontend/src/App.css — Basic Tailwind setup:

css
@tailwind base;
@tailwind components;
@tailwind utilities;

frontend/.env — Frontend environment variables:

env
VITE_API_URL=http://localhost:5000

Step 4: Testing

Test 1: Start Backend Server

bash
cd backend
npm run dev
```

Expected output:
```
🚀 Pipedrive Dashboard API running on http://localhost:5000

API Endpoints:
  GET  /api/dashboard - Dashboard metrics
  GET  /api/deals - All deals
  GET  /api/users - All users
  POST /api/cache/clear - Clear cache

Test 2: Test API Endpoints with curl

bash
# Test health check
curl http://localhost:5000/health

# Fetch dashboard metrics
curl http://localhost:5000/api/dashboard

# Fetch all deals
curl http://localhost:5000/api/deals

# Fetch users
curl http://localhost:5000/api/users

Expected response for /api/dashboard:

json
{
  "success": true,
  "data": {
    "overview": {
      "totalRevenue": 125000,
      "averageDealValue": 5000,
      "winRate": "45.5",
      "pipelineValue": 85000,
      "wonDealsCount": 25,
      "openDealsCount": 12
    },
    "dealsByStage": [...],
    "dealsByOwner": [...],
    "monthlyRevenue": [...]
  }
}
```

**Test 3: Verify Pipedrive API Connection**

Check backend logs:
```
Fetching deals from Pipedrive API...
✓ Fetched 37 deals
Fetching users from Pipedrive API...
✓ Fetched 5 users
✓ Metrics calculated

Test 4: Start Frontend Dashboard

bash
cd frontend
npm run dev

Open browser to http://localhost:5173

Test 5: Verify Dashboard Display

Check that dashboard shows:

  • ✓ Total Revenue metric card
  • ✓ Win Rate percentage
  • ✓ Average Deal Value
  • ✓ Pipeline Value with open deals count
  • ✓ Monthly revenue chart with 6 months data
  • ✓ Pipeline by stage with progress bars
  • ✓ Sales rep performance table

Test 6: Test Cache Functionality

First request (fetches from API):

bash
time curl http://localhost:5000/api/dashboard
# Should take 2-3 seconds

Second request (from cache):

bash
time curl http://localhost:5000/api/dashboard
# Should take < 100ms

Check logs for: ✓ Returning cached metrics

Test 7: Clear Cache and Refresh

bash
curl -X POST http://localhost:5000/api/cache/clear \
  -H "Content-Type: application/json" \
  -d '{"type": "all"}'

Click “Refresh Data” button in dashboard UI and verify metrics update.

Testing Checklist:

  • ✓ Backend starts without errors
  • ✓ Pipedrive API connection succeeds
  • ✓ All deals fetch successfully
  • ✓ Users/sales reps load
  • ✓ Dashboard metrics calculate correctly
  • ✓ Frontend displays all components
  • ✓ Charts render with data
  • ✓ Cache reduces API response time
  • ✓ Refresh button clears cache and reloads

Common Errors & Troubleshooting

Error 1: “401 Unauthorized” from Pipedrive API

Problem: API requests fail with 401 status code.

Solution: Invalid or missing API token. Verify token in .env:

env
PIPEDRIVE_API_TOKEN=your_actual_token_here

Steps to fix:

  1. Log into Pipedrive
  2. Go to Personal Preferences → API
  3. Generate new token if current one is invalid
  4. Copy token to .env file
  5. Restart backend server: npm run dev

Verify token works:

bash
curl "https://api.pipedrive.com/v1/users?api_token=YOUR_TOKEN"

Should return list of users, not 401 error.

Error 2: Empty or Missing Data in Dashboard

Problem: Dashboard loads but shows zero values for all metrics.

Solution: This happens when Pipedrive account has no data or API is returning empty arrays. Check:

Verify Pipedrive has data:

bash
curl "https://api.pipedrive.com/v1/deals?api_token=YOUR_TOKEN&limit=5"
```

Response should contain deals in `data` array. If empty:
- Add test deals in Pipedrive UI
- Check deal filters (status parameter)
- Verify user permissions allow reading deals

**Check backend logs:**
```
Fetching deals from Pipedrive API...
✓ Fetched 0 deals

If fetching 0 deals but Pipedrive has data, the pagination might be failing. Add debugging:

javascript
// In api/pipedrive.js, add console.log
async function fetchAllPages(endpoint, params = {}) {
  // ...
  console.log('Response:', JSON.stringify(response.data, null, 2));
  // ...
}

Also check deal status filter:

javascript
// Try different statuses
const deals = await getAllDeals('all_not_deleted'); // All deals
const deals = await getAllDeals('open'); // Only open
const deals = await getAllDeals('won'); // Only won

Error 3: CORS Error in Browser Console

Problem: Frontend shows error: Access to XMLHttpRequest blocked by CORS policy.

Solution: Backend not configured for frontend origin. Fix in src/server.js:

javascript
app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:5173',
  credentials: true,
}));

Verify .env has correct frontend URL:

env
FRONTEND_URL=http://localhost:5173

If using different port:

env
FRONTEND_URL=http://localhost:3000

For production, use actual domain:

env
FRONTEND_URL=https://dashboard.yourcompany.com

Quick fix for development (NOT for production):

javascript
// Temporary - allows all origins
app.use(cors());

Restart backend after .env changes.

Security Checklist

Protect API credentials and data with these practices:

  • Never commit API tokens — Add .env to .gitignore immediately. Use environment variables in production (AWS Secrets Manager, Heroku Config Vars, Vercel Environment Variables).
  • Rotate API tokens regularly — Generate new Pipedrive tokens quarterly. Old tokens remain valid until manually deleted in Pipedrive settings.
  • Implement backend authentication — Don’t expose Pipedrive API directly to frontend. The current setup correctly proxies requests through backend. Add authentication middleware:
javascript
  function authenticateRequest(req, res, next) {
    const apiKey = req.headers['x-api-key'];
    if (apiKey !== process.env.INTERNAL_API_KEY) {
      return res.status(401).json({ error: 'Unauthorized' });
    }
    next();
  }
  
  app.use('/api', authenticateRequest);
  • Use rate limiting — Prevent API abuse with express-rate-limit:
javascript
  import rateLimit from 'express-rate-limit';
  
  const limiter = rateLimit({
    windowMs: 15 * 60 * 1000, // 15 minutes
    max: 100, // max 100 requests per window
  });
  
  app.use('/api/', limiter);
  • Validate user permissions — If dashboard is multi-tenant, ensure users only see their own data:
javascript
  // Filter deals by user permissions
  const userDeals = allDeals.filter(deal => 
    req.user.permissions.includes(deal.user_id)
  );
  • Enable HTTPS only in production — Use SSL certificates (Let’s Encrypt, Cloudflare). Redirect HTTP to HTTPS:
javascript
  app.use((req, res, next) => {
    if (process.env.NODE_ENV === 'production' && !req.secure) {
      return res.redirect('https://' + req.headers.host + req.url);
    }
    next();
  });
  • Sanitize API responses — Remove sensitive fields before sending to frontend:
javascript
  const sanitizedDeals = deals.map(deal => ({
    id: deal.id,
    title: deal.title,
    value: deal.value,
    status: deal.status,
    // Exclude internal Pipedrive fields
  }));
  • Implement request logging — Track API usage for auditing:
javascript
  app.use((req, res, next) => {
    console.log(`[${new Date().toISOString()}] ${req.method} ${req.path}`);
    next();
  });
  • Use read-only API scopes — If Pipedrive allows granular permissions, restrict token to read-only access for dashboard use cases.
  • Cache sensitive data securely — If using Redis or database caching, encrypt cached data and set appropriate TTLs.
  • Monitor for suspicious activity — Set up alerts for unusual API usage patterns (sudden spike in requests, failed authentication attempts).

Leave a Comment

Your email address will not be published. Required fields are marked *