API integration
Numeralens never talks to a backend directly from a component. Every read goes through one interface:
UI → hooks (TanStack Query) → service facade → DataAdapter → your data
That means connecting your backend touches one layer, not 37 pages.
Option 1 — point at a REST API (no code)
Settings → Data source. Choose "REST API", enter your base URL, pick an auth
mode, press Test connection. The test calls GET {baseUrl}/reference-data
and reports exactly what it found or what failed. Settings persist in the browser
and invalidate all cached queries when saved.
For a permanent default, set in .env.local:
NEXT_PUBLIC_API_URL=https://api.yourcompany.com/v1
NEXT_PUBLIC_ENABLE_DEMO_MODE=false
NEXT_PUBLIC_AUTH_MODE=bearer
Option 2 — adapt the response shapes
If your endpoints exist but return different field names, edit
src/services/mappers.ts. It ships as identity functions and is deliberately the
only place where "their shape" becomes "our shape":
export const mapDashboard = (raw: unknown): DashboardSummary => {
const d = raw as Record<string, unknown>;
return {
kpis: (d.metrics as RawKpi[]).map(mapKpi),
revenueTrend: (d.revenue_series as RawPoint[]).map(mapPoint),
// …
};
};
Option 3 — write an adapter
For GraphQL, gRPC, a database client or a legacy SOAP gateway, implement
DataAdapter (src/services/adapters/types.ts) and return it from getAdapter()
in src/services/index.ts. 23 methods, all Promise-returning, all receiving
normalised FilterParams.
export function createGraphqlAdapter(client: Client): DataAdapter {
return {
getReferenceData: () => client.request(REFERENCE_QUERY).then(mapReferenceData),
getDashboard: (filters) => client.request(DASHBOARD_QUERY, toVars(filters)).then(mapDashboard),
// …
};
}
Endpoints the HTTP adapter calls
All relative to the base URL, all GET.
| Path | Returns |
|---|---|
/reference-data |
Companies, branches, departments, accounts, products, customers, vendors, currencies, categories |
/dashboard |
DashboardSummary |
/revenue, /expenses, /profitability, /cash-flow |
Analytics metrics |
/reports/profit-loss, /reports/balance-sheet, /reports/cash-flow-statement |
FinancialReport |
/general-ledger |
Paginated<LedgerEntry> |
/trial-balance |
TrialBalance |
/accounts-receivable, /accounts-payable |
Ageing reports |
/customers, /customers/:id |
CustomersReport, CustomerDetail |
/vendors, /vendors/:id |
VendorsReport, VendorDetail |
/transactions |
Paginated<Transaction> |
/transactions/:id |
{ transaction, related } — related is the complete journal entry, including the requested line, so debits and credits balance |
/budget |
BudgetReport |
/inventory |
InventoryReport |
/search?q= |
SearchResult[] |
/notifications |
Notification[] |
The demo API routes under src/app/api/ implement all of these against the mock
engine — read them as a reference implementation, or as fixtures for your own
backend's contract tests.
Query parameters
Filters are serialised by buildQuery. A typical request:
GET /revenue?from=2026-01-01&to=2026-12-31&comparison=previous_year
&companyId=co-1&departmentId=dp-sales&granularity=monthly
Table endpoints add page, pageSize, search, sortBy, sortDir.
Transactions add type, status, minAmount, maxAmount, reference.
Response envelope
Both of these are accepted:
{ "data": { … } }
{ … }
Errors
Non-2xx responses become a typed ApiError with a code:
| Code | Trigger | UI behaviour |
|---|---|---|
unauthorized |
401 / 403 | "You don't have access to this data" |
not_found |
404 | "Not found" |
timeout |
Request exceeded the timeout | Retry plus a link to data-source settings |
network |
Fetch threw | "Can't reach the data source" plus settings link |
server |
Other non-2xx | Generic failure with retry |
invalid_data |
Payload failed validation | "Returned something unexpected" |
Each widget catches its own error, so one failing endpoint does not blank the page.
Authentication
bearer— the adapter awaitsgetAuthToken()fromsrc/services/auth-token.tsand sendsAuthorization: Bearer …. Replace the body with your provider's accessor.cookie— requests go out withcredentials: "include". Your API must send permissive CORS headers plusAccess-Control-Allow-Credentials: true.
Checklist
GET /reference-datareturns accounts, customers, vendors, departments.- Amounts are numbers, not strings; dates are ISO
yyyy-MM-dd. - Percentages are whole numbers (
14.8= 14.8%). - Paginated endpoints return
{ items, total, page, pageSize }. - CORS allows your dashboard origin.
- Test connection in Settings → Data source succeeds.