Case study: F1 dashboard
Context
Most F1 sites answer “what happened?” Only after wading through ads, autoplay video, and a navigation system that treats every driver stat as a separate page. I wanted one page that answers the questions I actually ask during a race weekend: what sessions are coming up, who’s leading the standings, what’s the minimum I need to know right now.
The secondary goal was seeing how far you could get by merging open data sources. Ergast/Jolpica for historical races, OpenF1 for session details and telemetry, Wikipedia for circuit descriptions, f1db for biographical data. Turns out: far enough to build something more comprehensive than any single API provides.
Problem
Build a dashboard that loads fast, reads at a glance, and covers both “what’s happening now?” and “what happened in 1998?” without requiring ten tabs or five minutes of navigation. Historical data is stable; current season updates after every race. The UI needed to degrade gracefully when an upstream source is slow or down — not crash.
Approach
1. Normalize upstream data first
The four sources use different IDs, different name formats, and different structural assumptions. Jolpica calls him max_verstappen; OpenF1 calls him VER; f1db uses his full legal name. Constructor lineages (Toleman → Benetton → Renault → Alpine) are treated as separate entities in some APIs and a single thread in others. Sprint races introduced in 2021 have no dedicated fields in older APIs. Lap times don’t exist for seasons before the 1980s.
All of this is handled in a normalization layer on the server so the UI can assume clean, consistent data and never special-case an API quirk.
2. SQLite as the caching layer
Historical seasons don’t change. Fetching all 70+ seasons from Jolpica once and caching forever is the correct move — subsequent responses are instant, and the cache survives container restarts. Current season data always refetches (standings change after each race). OpenF1 enrichment caches with a 1-hour TTL for live sessions, permanently once a race is finished.
SQLite in WAL mode handles concurrent reads without blocking, batch-inserts the full historical dataset in under a minute, and adds no operational overhead — no separate database process, no connection strings, no migrations that need applying to a remote host.
3. Performance as part of the design
TanStack Query on the client caches aggressively — stale data is better than a loading spinner for a dashboard. Routes are code-split (30+ pages, each lazy-loaded). Static assets get a 1-year cache TTL. The Docker container serves both the API and the client from one process, which eliminates CORS and removes a network roundtrip.
The result: sub-100ms responses for cached historical data, which covers essentially all browsing outside the current season.
4. Data source resilience
The server becoming the single point of contact for all four upstream APIs is a feature, not a tradeoff. If Jolpica goes down, the full historical cache is still intact. If OpenF1 is slow, current-season enrichment degrades gracefully — session times might be missing, everything else still works. Upstream instability (Jolpica has broken its API twice) only matters once, not on every user request.
5. Daily Challenge mode
A shared-seed daily run: the challenge seed is a hash of the UTC date, which generates the same random historical race for everyone. Same race, same prompts, resets at midnight. Minimal to implement on top of the existing deterministic historical dataset.
What’s missing
- Per-module error boundaries — if one upstream call fails, the whole page can currently break. Session data failing shouldn’t take standings with it.
- Staleness indicators — no UI signal for “this was last updated 2 hours ago.” For live session data that matters.
- Integration tests for upstream shape — Jolpica breaking its API format took two weeks to notice. A lightweight schema check on the normalized output would catch that immediately.
- Live timing — intentionally excluded. It needs WebSocket infrastructure and a data license; it’s a different project.