Skip to main content

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

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:

  1. Your app calls /api/integrations/\{slug\}/request
  2. Informer adds auth headers (tokens, OAuth, etc.)
  3. Informer forwards the request to the external API
  4. 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:

VariableDescription
$user.usernameLogin name
$user.emailEmail address
$user.custom.xxxCustom user field value
$tenant.idTenant identifier
$report.idReport 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.