API Mashup
Combine datasets, integrations, and saved queries in a single app with responsive layouts and loading states.
What You'll Build
A comprehensive dashboard that:
- Queries a dataset (orders)
- Executes a saved query (monthly summary)
- Calls an integration (partner API)
- Reads all three through typed dependency slots in a single server route
- Shows loading states for each data source
- Handles errors gracefully
- Uses a multi-panel responsive layout
Prerequisites
- Completed Integration Dashboard
- A saved query in Informer
- A dataset, a saved query, and an integration to bind to your dependency slots
Example Prompts for Claude Code
Build the mashup:
"Create an executive dashboard with 3 panels. First panel queries an orders dataset for revenue metrics. Second panel executes a monthly-summary saved query and shows it as a line chart. Third panel calls a partner-api integration. Each panel should show its own loading status. Declare three dependency slots in informer.yaml — an
ordersdataset, asummaryquery, and apartnerintegration — and read all three through a single server route that returns a combined object. Load the panels in the frontend, and have the server route fetch the sources in parallel so no resource IDs are hardcoded in the browser."
Add polish:
"Add a status indicator (loading/success/error) to each panel header. Use color-coded badges."
"Make the layout responsive — stack panels vertically on mobile, show 2 columns on desktop."
Improve error handling:
"If one data source fails, the others should still work. Show a friendly error message in the failed panel but keep the rest of the dashboard functional."
What Claude Will Generate
Claude will create a multi-panel layout with parallel data loading. Here's what the generated code looks like:
Data Access Configuration
The app declares each data source as a typed dependency slot in informer.yaml. The installer (or each slot's defaultBinding) points a slot at a real resource, and your server route refers to it by slot name — no dataset, query, or integration IDs ever appear in the frontend:
dependencies:
orders:
target: dataset
# UUID of your orders dataset — look it up with `GET /api/datasets-list`.
defaultBinding: 7d5a9b1e-0c83-4bde-9e2a-3a4b5c6d7e8f
summary:
target: query
# UUID of your saved query — look it up with `GET /api/queries-list`.
defaultBinding: 9a8b7c6d-5e4f-3a2b-1c0d-fedcba987654
partner:
target: integration
# UUID of your integration — look it up with `GET /api/integrations-list`.
defaultBinding: 5a6b7c8d-9e0f-1234-5678-9abcdef01234
See the informer.yaml reference for the full slot model.
Multi-Panel HTML
Claude will scaffold a panel-based layout:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Executive Dashboard</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="dashboard">
<header>
<h1>Executive Dashboard</h1>
<p class="subtitle">Real-time data from multiple sources</p>
</header>
<div class="filters">
<div class="filter-group">
<label for="dateRange">Date Range</label>
<select id="dateRange">
<option value="30" selected>Last 30 days</option>
<option value="90">Last 90 days</option>
<option value="365">Last year</option>
</select>
</div>
<button id="refresh" class="btn-primary">Refresh</button>
</div>
<!-- Orders Panel -->
<div class="panel">
<div class="panel-header">
<h2>Orders</h2>
<span class="status" id="ordersStatus">Loading...</span>
</div>
<div class="metrics">
<div class="metric-card">
<span class="metric-value" id="ordersRevenue">--</span>
<span class="metric-label">Revenue</span>
</div>
<div class="metric-card">
<span class="metric-value" id="ordersCount">--</span>
<span class="metric-label">Count</span>
</div>
</div>
</div>
<!-- Monthly Summary Panel (Saved Query) -->
<div class="panel">
<div class="panel-header">
<h2>Monthly Summary</h2>
<span class="status" id="summaryStatus">Loading...</span>
</div>
<div id="summaryChart" style="width: 100%; height: 300px;"></div>
</div>
<!-- Partner Sales Panel (Integration) -->
<div class="panel">
<div class="panel-header">
<h2>Partner Sales</h2>
<span class="status" id="partnerStatus">Loading...</span>
</div>
<div class="metrics">
<div class="metric-card">
<span class="metric-value" id="partnerRevenue">--</span>
<span class="metric-label">Revenue</span>
</div>
<div class="metric-card">
<span class="metric-value" id="partnerCount">--</span>
<span class="metric-label">Orders</span>
</div>
</div>
</div>
<!-- Combined Chart -->
<div class="panel panel-wide">
<div class="panel-header">
<h2>Revenue Breakdown</h2>
</div>
<div id="combinedChart" style="width: 100%; height: 400px;"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>
Panel Styles
Claude will add responsive panel styling with status indicators:
/* ... previous styles ... */
.panel {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
}
.panel-wide {
grid-column: 1 / -1;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.panel-header h2 {
font-size: 18px;
font-weight: 600;
}
.status {
font-size: 13px;
padding: 4px 10px;
border-radius: 12px;
font-weight: 500;
}
.status.loading {
background: #fef3c7;
color: #92400e;
}
.status.success {
background: #d1fae5;
color: #065f46;
}
.status.error {
background: #fee2e2;
color: #991b1b;
}
[data-theme="dark"] .status.loading {
background: #78350f;
color: #fef3c7;
}
[data-theme="dark"] .status.success {
background: #064e3b;
color: #d1fae5;
}
[data-theme="dark"] .status.error {
background: #7f1d1d;
color: #fee2e2;
}
@media (min-width: 1024px) {
.dashboard {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
header,
.filters,
.panel-wide {
grid-column: 1 / -1;
}
}
@media print {
.filters { display: none; }
.status { display: none; }
}
Server Route
All three sources are read in one server route that calls the typed dependency slots and returns a combined object. Because the route fetches the dataset, the saved query, and the integration on the server, no resource IDs, configIds, or Elasticsearch queries ever reach the browser. It uses Promise.allSettled so one failed source doesn't sink the others:
// server/dashboard.js
export async function GET({ context, request }) {
const days = parseInt(request.query.days || '30', 10);
const dateFilter = buildDateFilter(days);
const [orders, summary, partner] = await Promise.allSettled([
loadOrders(context, dateFilter),
loadSummary(context, days),
loadPartner(context, dateFilter)
]);
// Each section reports its own status so the frontend can render
// partial results when one source fails.
return {
orders: settled(orders),
summary: settled(summary),
partner: settled(partner)
};
}
async function loadOrders(context, dateFilter) {
const filters = [];
if (dateFilter) {
filters.push({
range: { orderDate: { gte: dateFilter.start, lte: dateFilter.end } }
});
}
// dataset slot → search(esQuery)
const result = await context.orders.search({
query: { bool: { filter: filters } },
size: 0,
aggs: {
totalRevenue: { sum: { field: 'amount' } },
orderCount: { value_count: { field: 'id' } }
}
});
const aggs = result.aggregations;
return {
revenue: aggs.totalRevenue.value,
count: aggs.orderCount.value
};
}
async function loadSummary(context, days) {
// query slot → execute(params)
const result = await context.summary.execute({ days });
// Assume result format: { data: [{ month: 'Jan', revenue: 12345 }, ...] }
return result.data || [];
}
async function loadPartner(context, dateFilter) {
const params = {};
if (dateFilter) {
params.start_date = dateFilter.start;
params.end_date = dateFilter.end;
}
// integration slot → request({ method, url, params, data })
// Returns the upstream JSON directly; throws on a 4xx/5xx upstream.
const sales = await context.partner.request({
method: 'GET',
url: '/v1/sales',
params
});
return {
revenue: sales.total_revenue || 0,
count: sales.count || 0
};
}
function buildDateFilter(days) {
if (!days) return null;
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]
};
}
function settled(outcome) {
return outcome.status === 'fulfilled'
? { ok: true, data: outcome.value }
: { ok: false, error: String(outcome.reason) };
}
Each slot is declared in informer.yaml (see Data Access Configuration above) and arrives pre-typed on context — context.orders exposes search(), context.summary exposes execute(), and context.partner exposes request(). See the server routes reference and the informer.yaml dependencies model for details.
Parallel Data Loading
The frontend calls the single server route and fans the combined result out into the three panels. There are no resource IDs, no Elasticsearch queries, and no integration paths in the browser:
window.informerReady = false;
const theme = window.__INFORMER__?.theme || 'light';
document.documentElement.setAttribute('data-theme', theme);
let currentFilters = { dateRange: '30' };
async function init() {
try {
// Wire up controls
document.getElementById('refresh').addEventListener('click', loadAllData);
document.getElementById('dateRange').addEventListener('change', (e) => {
currentFilters.dateRange = e.target.value;
});
// Load all data sources
await loadAllData();
window.informerReady = true;
} catch (err) {
console.error('Initialization failed:', err);
}
}
async function loadAllData() {
setStatus('ordersStatus', 'loading');
setStatus('summaryStatus', 'loading');
setStatus('partnerStatus', 'loading');
try {
// The server route reads all three dependency slots and returns a
// combined object — one round trip, no resource IDs in the frontend.
const days = parseInt(currentFilters.dateRange);
const response = await fetch(`/api/_server/dashboard?days=${days}`);
if (!response.ok) throw new Error(`Dashboard load failed: ${response.status}`);
const { orders, summary, partner } = await response.json();
renderOrders(orders);
renderSummary(summary);
renderPartner(partner);
} catch (err) {
console.error('Dashboard error:', err);
setStatus('ordersStatus', 'error', 'Failed');
setStatus('summaryStatus', 'error', 'Failed');
setStatus('partnerStatus', 'error', 'Failed');
window.ordersData = { revenue: 0, count: 0 };
window.summaryData = [];
window.partnerData = { revenue: 0, count: 0 };
}
// After all panels load, render combined chart
renderCombinedChart();
}
function renderOrders(orders) {
if (!orders.ok) {
console.error('Orders error:', orders.error);
setStatus('ordersStatus', 'error', 'Failed');
document.getElementById('ordersRevenue').textContent = '--';
document.getElementById('ordersCount').textContent = '--';
window.ordersData = { revenue: 0, count: 0 };
return;
}
const { revenue, count } = orders.data;
document.getElementById('ordersRevenue').textContent = formatCurrency(revenue);
document.getElementById('ordersCount').textContent = count.toLocaleString();
setStatus('ordersStatus', 'success', 'Loaded');
// Store for combined chart
window.ordersData = { revenue, count };
}
function renderSummary(summary) {
if (!summary.ok) {
console.error('Summary query error:', summary.error);
setStatus('summaryStatus', 'error', 'Failed');
window.summaryData = [];
return;
}
const data = summary.data;
renderSummaryChart(data);
setStatus('summaryStatus', 'success', 'Loaded');
// Store for combined chart
window.summaryData = data;
}
function renderPartner(partner) {
if (!partner.ok) {
console.error('Partner API error:', partner.error);
setStatus('partnerStatus', 'error', 'Failed');
document.getElementById('partnerRevenue').textContent = '--';
document.getElementById('partnerCount').textContent = '--';
window.partnerData = { revenue: 0, count: 0 };
return;
}
const { revenue, count } = partner.data;
document.getElementById('partnerRevenue').textContent = formatCurrency(revenue);
document.getElementById('partnerCount').textContent = count.toLocaleString();
setStatus('partnerStatus', 'success', 'Loaded');
// Store for combined chart
window.partnerData = { revenue, count };
}
function renderSummaryChart(data) {
const chart = echarts.init(document.getElementById('summaryChart'), theme === 'dark' ? 'dark' : null);
const months = data.map(d => d.month);
const revenues = data.map(d => d.revenue);
chart.setOption({
tooltip: { trigger: 'axis' },
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: months
},
yAxis: {
type: 'value',
axisLabel: {
formatter: (val) => {
if (val >= 1000000) return `$${(val / 1000000).toFixed(1)}M`;
if (val >= 1000) return `$${(val / 1000).toFixed(0)}K`;
return `$${val}`;
}
}
},
series: [{
name: 'Revenue',
type: 'line',
data: revenues,
smooth: true,
itemStyle: { color: '#6366f1' }
}]
});
window.addEventListener('resize', () => chart.resize());
}
function renderCombinedChart() {
const chart = echarts.init(document.getElementById('combinedChart'), theme === 'dark' ? 'dark' : null);
const ordersRev = window.ordersData?.revenue || 0;
const partnerRev = window.partnerData?.revenue || 0;
chart.setOption({
tooltip: {
trigger: 'item',
formatter: (params) => {
return `${params.name}<br/>${formatCurrency(params.value)} (${params.percent}%)`;
}
},
legend: { top: '5%', left: 'center' },
series: [{
name: 'Revenue Source',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: theme === 'dark' ? '#1e293b' : '#fff',
borderWidth: 2
},
label: { show: false },
emphasis: {
label: {
show: true,
fontSize: 18,
fontWeight: 'bold'
}
},
data: [
{ value: ordersRev, name: 'Internal Orders' },
{ value: partnerRev, name: 'Partner Sales' }
]
}]
});
window.addEventListener('resize', () => chart.resize());
}
function setStatus(elementId, state, text) {
const el = document.getElementById(elementId);
el.className = `status ${state}`;
el.textContent = text || state;
}
function formatCurrency(val) {
if (isNaN(val)) return '--';
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0
}).format(val);
}
init();
Query Execution Pattern
Saved queries are executed through a query dependency slot — call context.<slot>.execute(params) from a server route instead of fetching /api/queries/{id}/_execute from the browser:
// server/summary.js
export async function GET({ context }) {
// `summary` is a `target: query` slot declared in informer.yaml.
const result = await context.summary.execute({
startDate: '2024-01-01',
endDate: '2024-12-31',
region: 'North'
});
return result.data; // [{ col1: val1, col2: val2 }, ...]
}
The frontend then calls fetch('/api/_server/summary') — the query ID stays on the server. See the informer.yaml dependencies model for how the slot is declared.
Parameters are defined in the query's configuration and can be:
- Text - Free-form strings
- Number - Numeric values
- Date - ISO date strings
- List - Single or multi-select values
Loading States
The dashboard uses three status states:
setStatus('ordersStatus', 'loading'); // Yellow: data loading
setStatus('ordersStatus', 'success', 'Loaded'); // Green: success
setStatus('ordersStatus', 'error', 'Failed'); // Red: error
This provides visual feedback for each data source independently.
Error Resilience
The server/dashboard.js route uses Promise.allSettled so all three sources are attempted even if one fails:
const [orders, summary, partner] = await Promise.allSettled([
loadOrders(context, dateFilter), // dataset slot — might fail
loadSummary(context, days), // query slot — might fail
loadPartner(context, dateFilter) // integration slot — might fail
]);
// Each section is returned with its own ok/error flag, so the frontend
// can render partial results instead of a blank dashboard.
return {
orders: settled(orders, { revenue: 0, count: 0 }),
summary: settled(summary, []),
partner: settled(partner, { revenue: 0, count: 0 })
};
Without allSettled, a single failed dependency would break the entire dashboard. The frontend reads each section's ok flag and renders the panels that succeeded.
Test Locally
npm run dev
Verify:
- All three panels load in parallel
- Status indicators update correctly
- Errors are handled gracefully
- Combined chart renders after all data loads
Deploy
npm run deploy
Next Steps
Continue to AI-Enhanced Dashboard to add AI capabilities with tools and chat integration.