FinBoard is a modular, client-side configurable finance dashboard built with Next.js (App Router). It allows users to dynamically create, configure, persist, and visualize real-time financial data from multiple APIs using customizable widgets.
The architecture emphasizes:
- Separation of concerns
- Scalability
- API-agnostic data handling
- Resilient real-time updates
- Persistent, user-driven UI state
┌──────────────────────────────┐
│ User (UI) │
└──────────────┬───────────────┘
│
▼
┌──────────────────────────────┐
│ Next.js App Router │
│ (layout.tsx, page.tsx) │
└──────────────┬───────────────┘
│
▼
┌────────────────────────────────────────────┐
│ Presentation Layer │
│ Navbar | Modals | Widgets | Charts | Tables│
└──────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ State Management (Zustand) │
│ widgetStore | themeStore │
└──────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ Data Processing & Caching Layer │
│ apiCache | dataMapper │
└──────────────┬─────────────────────────────┘
│
▼
┌────────────────────────────────────────────┐
│ External Financial APIs │
│ Finnhub | Custom APIs | Others │
└────────────────────────────────────────────┘
## Entry Points
### 1. Application Entry
```txt
src/app/layout.tsx
Responsibilities:
- Global providers (ThemeProvider)
- Navbar rendering
- Root HTML/body structure
src/app/page.tsxResponsibilities:
- Render widget grid
- Drag & drop layout handling
- Modal mounting
- Widget lifecycle orchestration
store/
├── WidgetStore.ts
└── themeStore.ts- Widget CRUD (add, remove, update)
- Widget ordering (drag & drop)
- Import / Export dashboard configuration
- Persistent storage via
zustand/persist
Design Decision
-
Zustand chosen over Redux for:
- Minimal boilerplate
- UI-heavy state
- Faster iteration
- Global dark/light mode state
- Persistent theme storage
- Cross-tab synchronization
- DOM-safe application via ThemeProvider
components/
├── layout/
│ └── Navbar.tsx
│ └── TemplateModal.tsx
├── widget/
│ ├── AddWidget.tsx
│ ├── AddWidgetModal.tsx
│ ├── DataWidget.tsx
│ ├── EmptyDashboard.tsx
│ ├── DraggableWidget.tsx
│ ├── WidgetCard.tsx
│ ├── StockChart.tsx
├── ui/
│ ├── ModernTable.tsx
│ ├── Modal.tsx
│ ├── Button.tsx
│ └── ThemeToggle.tsx
└── ThemeProvider.tsx- Pure components
- No direct API logic in UI
- Reusable primitives
- Responsive-first (Tailwind)
Navbar → AddWidgetModal
→ API Test
→ Data Mapper (field discovery)
→ widgetStore.addWidget()
→ Persist to localStorage
widgetStore
↓
DataWidget
├── cachedFetch()
├── auto-refresh timer
├── dataMapper (path-based mapping)
↓
WidgetCard
↓
Chart | Table | Card
utils/
├── apiCache.ts
└── dataMapper.ts- In-memory TTL caching
- Rate limiting per API domain
- Request deduplication
- Batch stock fetching
- Error isolation
Why this matters
- Prevents API quota exhaustion
- Improves performance
- Enables real-time dashboards safely
- Recursive JSON structure analysis
- Field path discovery
- Type inference
- Path-based value extraction
- Smart formatting (currency, %, compact numbers)
Key Design
Widgets are API-agnostic — they only care about field paths, not API shape.
@dnd-kit
DraggableWidget
↓
DndContext (page.tsx)
↓
onDragEnd(activeId, overId)
↓
widgetStore.reorderWidgets()
↓
Persisted layout update
ThemeToggle
↓
themeStore
↓
ThemeProvider
↓
<html class="dark | light">
- No hydration mismatch
- No flash of unstyled content (FOUC)
- Cross-tab theme sync
- Dynamic widget creation
- Field-level API mapping
- Drag & drop layout
- Auto-refresh per widget
- API caching & rate limiting
- Import / Export dashboard
- Dark / Light theme
- Responsive layout
- Error & loading states
- WebSocket live data
- Dashboard templates
- Widget editing
- Chart axis configuration
- Role-based dashboards
| Layer | Strategy |
|---|---|
| API | Retry + rate-limit detection |
| Widget | Isolated failure per widget |
| UI | Skeletons + error messages |
| Import | Schema validation |
- Widget-level auto-refresh
- Cached API responses
- Request deduplication
- Lazy rendering of charts
- Minimal global re-renders
- Single source of truth
- Config-driven UI
- API-agnostic widgets
- Fail independently
- Persist everything