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:
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:
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:
{
"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:
echo "node_modules/
.env
*.log" > .gitignore
Step 2: Configuration
backend/.env — Store Pipedrive API credentials:
# 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:
- Log into Pipedrive
- Click your profile icon → Personal preferences
- Go to API tab
- Click Generate new token
- Copy the token (starts with letters and numbers)
- Your company domain is from your URL:
https://yourcompany.pipedrive.com
utils/cache.js — Simple in-memory cache to avoid rate limits:
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:
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:
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:
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:
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:
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:
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:
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:
@tailwind base;
@tailwind components;
@tailwind utilities;
frontend/.env — Frontend environment variables:
VITE_API_URL=http://localhost:5000
Step 4: Testing
Test 1: Start Backend Server
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
# 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:
{
"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
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):
time curl http://localhost:5000/api/dashboard
# Should take 2-3 seconds
Second request (from cache):
time curl http://localhost:5000/api/dashboard
# Should take < 100ms
Check logs for: ✓ Returning cached metrics
Test 7: Clear Cache and Refresh
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:
PIPEDRIVE_API_TOKEN=your_actual_token_here
Steps to fix:
- Log into Pipedrive
- Go to Personal Preferences → API
- Generate new token if current one is invalid
- Copy token to
.envfile - Restart backend server:
npm run dev
Verify token works:
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:
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:
// 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:
// 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:
app.use(cors({
origin: process.env.FRONTEND_URL || 'http://localhost:5173',
credentials: true,
}));
Verify .env has correct frontend URL:
FRONTEND_URL=http://localhost:5173
If using different port:
FRONTEND_URL=http://localhost:3000
For production, use actual domain:
FRONTEND_URL=https://dashboard.yourcompany.com
Quick fix for development (NOT for production):
// 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
.envto.gitignoreimmediately. 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:
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:
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:
// 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:
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:
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:
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).

Huzaifa Asif is a dedicated software and integration specialist at TheSportsAngel, focused on making complex API and system integrations simple and actionable. With over 3+ years of hands-on experience in backend development, CRM/ERP connectivity, and third-party platform integrations, he transforms technical architecture into clear, step-by-step coding guides that both developers and non-technical users can follow.



