Integration Dashboard
Pull data from external REST APIs via Informer integrations and combine it with internal dataset data.
What You'll Build
A dashboard that:
- Queries an external API via an Informer integration
- Combines external data with Informer dataset data
- Handles integration auth (tokens, headers) via
informer.yaml - Shows error handling for upstream API failures
Prerequisites
- Completed Multi-Dataset Dashboard
- An integration configured in Informer (e.g., REST API, Salesforce, QuickBooks)
Example Prompts for Claude Code
Add integration data:
"Query the partner-api integration at /v1/sales endpoint and combine the revenue with our internal orders. Show both as separate metric cards plus a pie chart showing the revenue split. Update informer.yaml to grant access to partner-api."
Salesforce example:
"Pull our CRM pipeline data from the salesforce integration. Query the Opportunity object for all Closed Won deals in the current date range. Show the total value and count alongside our orders data."
Error handling:
"Add defensive error handling for the integration request. If the external API is down, show a graceful fallback message instead of breaking the whole dashboard."
What is an Integration?
Integrations are authenticated connections to external APIs. Informer acts as a secure proxy:
- Your app calls
/api/integrations/\{slug\}/request - Informer adds auth headers (tokens, OAuth, etc.)
- Informer forwards the request to the external API
- Response is returned to your app
Benefits:
- Secure - API credentials never exposed to client-side JavaScript
- Centralized - One integration serves multiple apps
- Row-level security - Inject user-specific tokens via
informer.yaml
Data Access Configuration
Update informer.yaml to grant access to the integration:
access:
datasets:
- admin:orders
integrations:
- id: partner-api
headers:
Authorization: Bearer $user.custom.partnerToken
params:
tenant_id: $tenant.id
Variable Expansion
Variables are expanded server-side, keeping credentials secure:
| Variable | Description |
|---|---|
$user.username | Login name |
$user.email | Email address |
$user.custom.xxx | Custom user field value |
$tenant.id | Tenant identifier |
$report.id | Report UUID |
In this example:
$user.custom.partnerToken- Each user has their own API token stored in a custom field$tenant.id- The tenant ID is sent as a query parameter
What Claude Will Generate
When you ask Claude to add integration support, it will handle the API proxy pattern, error handling, and data combination. Here's what the generated code looks like:
Integration Request Pattern
const response = await fetch('/api/integrations/partner-api/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: '/v1/sales', // Path relative to integration's base URL
method: 'GET', // HTTP method
params: {
start_date: '2024-01-01',
end_date: '2024-12-31'
}
})
});
const result = await response.json();
// Response structure:
// result.status - HTTP status from upstream API
// result.data - Response body from the API
// result.error - true if upstream returned error status
Complete Integration Example
Claude will generate integration query logic like this:
window.informerReady = false;
const theme = window.__INFORMER__?.theme || 'light';
document.documentElement.setAttribute('data-theme', theme);
const ORDERS_DATASET = 'admin:orders';
const PARTNER_INTEGRATION = 'partner-api';
let currentFilters = { dateRange: '30' };
async function init() {
try {
await loadData();
document.getElementById('dateRange').addEventListener('change', (e) => {
currentFilters.dateRange = e.target.value;
});
document.getElementById('refresh').addEventListener('click', loadData);
window.informerReady = true;
} catch (err) {
console.error('Failed to load data:', err);
showError(err.message);
}
}
async function loadData() {
const dateFilter = buildDateFilter();
// Query both sources in parallel
const [ordersResult, partnerResult] = await Promise.all([
queryOrders(dateFilter),
queryPartnerAPI(dateFilter)
]);
// Combine and display
updateMetrics(ordersResult, partnerResult);
renderChart(ordersResult, partnerResult);
}
function buildDateFilter() {
if (currentFilters.dateRange === 'all') {
return null;
}
const days = parseInt(currentFilters.dateRange);
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - days);
return {
start: startDate.toISOString().split('T')[0],
end: endDate.toISOString().split('T')[0]
};
}
async function queryOrders(dateFilter) {
const filters = [];
if (dateFilter) {
filters.push({
range: {
orderDate: {
gte: dateFilter.start,
lte: dateFilter.end
}
}
});
}
const response = await fetch(`/api/datasets/${ORDERS_DATASET}/_search`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: { bool: { filter: filters } },
size: 0,
aggs: {
totalRevenue: { sum: { field: 'amount' } },
orderCount: { value_count: { field: 'id' } }
}
})
});
if (!response.ok) {
throw new Error(`Orders query failed: ${response.status}`);
}
return response.json();
}
async function queryPartnerAPI(dateFilter) {
const params = {};
if (dateFilter) {
params.start_date = dateFilter.start;
params.end_date = dateFilter.end;
}
const response = await fetch(`/api/integrations/${PARTNER_INTEGRATION}/request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: '/v1/sales',
method: 'GET',
params
})
});
if (!response.ok) {
throw new Error(`Integration request failed: ${response.status}`);
}
const result = await response.json();
// Check if upstream API returned an error
if (result.error) {
console.error('Partner API error:', result.data);
return { totalRevenue: 0, orderCount: 0 };
}
// Parse partner API response
// Assume response format: { sales: [...], total_revenue: 123456, count: 789 }
return {
totalRevenue: result.data.total_revenue || 0,
orderCount: result.data.count || 0,
sales: result.data.sales || []
};
}
function updateMetrics(ordersResult, partnerResult) {
const ordersAggs = ordersResult.aggregations;
const combinedRevenue = ordersAggs.totalRevenue.value + partnerResult.totalRevenue;
const combinedOrders = ordersAggs.orderCount.value + partnerResult.orderCount;
document.getElementById('totalRevenue').textContent = formatCurrency(combinedRevenue);
document.getElementById('orderCount').textContent = combinedOrders.toLocaleString();
document.getElementById('internalRevenue').textContent = formatCurrency(ordersAggs.totalRevenue.value);
document.getElementById('partnerRevenue').textContent = formatCurrency(partnerResult.totalRevenue);
}
function renderChart(ordersResult, partnerResult) {
const chart = echarts.init(document.getElementById('chart'), theme === 'dark' ? 'dark' : null);
chart.setOption({
tooltip: {
trigger: 'item',
formatter: (params) => {
return `${params.name}<br/>${formatCurrency(params.value)} (${params.percent}%)`;
}
},
legend: {
orient: 'vertical',
left: 'left'
},
series: [{
name: 'Revenue',
type: 'pie',
radius: '50%',
data: [
{ value: ordersResult.aggregations.totalRevenue.value, name: 'Internal Orders' },
{ value: partnerResult.totalRevenue, name: 'Partner Sales' }
],
emphasis: {
itemStyle: {
shadowBlur: 10,
shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)'
}
}
}]
});
window.addEventListener('resize', () => chart.resize());
}
function formatCurrency(val) {
if (isNaN(val)) return '--';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(val);
}
function showError(message) {
document.querySelector('.dashboard').innerHTML =
`<div style="padding: 40px; text-align: center; color: #ef4444;">
Error: ${message}
</div>`;
}
init();
HTML for Combined Metrics
Claude will add metric cards for both internal and partner data:
<div class="metrics">
<div class="metric-card">
<span class="metric-value" id="totalRevenue">--</span>
<span class="metric-label">Total Revenue</span>
</div>
<div class="metric-card">
<span class="metric-value" id="orderCount">--</span>
<span class="metric-label">Total Orders</span>
</div>
<div class="metric-card">
<span class="metric-value" id="internalRevenue">--</span>
<span class="metric-label">Internal Revenue</span>
</div>
<div class="metric-card">
<span class="metric-value" id="partnerRevenue">--</span>
<span class="metric-label">Partner Revenue</span>
</div>
</div>
<div class="content">
<h2>Revenue Sources</h2>
<div id="chart" style="width: 100%; height: 400px;"></div>
</div>
Error Handling
Integration requests can fail in multiple ways:
Network Errors (Informer Unreachable)
try {
const response = await fetch('/api/integrations/partner-api/request', { ... });
} catch (err) {
console.error('Network error:', err);
// Show user-friendly message
}
Informer API Errors (403, 404, etc.)
const response = await fetch('/api/integrations/partner-api/request', { ... });
if (!response.ok) {
const error = await response.json();
console.error('Informer API error:', error.message);
}
Upstream API Errors (External Service Down)
const result = await response.json();
if (result.error) {
// Upstream returned 4xx or 5xx
console.error('Upstream error:', result.status, result.data);
}
Defensive Pattern
async function queryPartnerAPI(dateFilter) {
try {
const response = await fetch(`/api/integrations/${PARTNER_INTEGRATION}/request`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: '/v1/sales',
method: 'GET',
params: dateFilter ? { start_date: dateFilter.start, end_date: dateFilter.end } : {}
})
});
if (!response.ok) {
console.error('Integration request failed:', response.status);
return fallbackData();
}
const result = await response.json();
if (result.error) {
console.error('Partner API error:', result.status, result.data);
return fallbackData();
}
return parsePartnerResponse(result.data);
} catch (err) {
console.error('Network error:', err);
return fallbackData();
}
}
function fallbackData() {
return { totalRevenue: 0, orderCount: 0, sales: [] };
}
Salesforce Example
For a real-world Salesforce integration:
async function querySalesforce(dateFilter) {
const soql = `
SELECT Id, Name, Amount, StageName, CloseDate
FROM Opportunity
WHERE StageName = 'Closed Won'
AND CloseDate >= ${dateFilter.start}
AND CloseDate <= ${dateFilter.end}
`;
const response = await fetch('/api/integrations/salesforce/request', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: '/data/v59.0/query',
method: 'GET',
params: { q: soql }
})
});
const result = await response.json();
if (result.error) {
throw new Error(`Salesforce error: ${result.data.message}`);
}
// result.data.records = [{ Id, Name, Amount, ... }, ...]
const records = result.data.records;
const totalRevenue = records.reduce((sum, r) => sum + (r.Amount || 0), 0);
return { totalRevenue, records };
}
Test Locally
npm run dev
Verify:
- Integration request succeeds (check network tab)
- Error handling gracefully degrades if API fails
- Combined metrics display correctly
Deploy
npm run deploy
Next Steps
Continue to API Mashup to learn how to combine datasets, integrations, and saved queries in one app.