Overview
Six decisions that shaped Weather Now. Each has context, choice, why, and trade-off — no jargon wall.
ADR-001: Keyless Open-Meteo for Weather and Search
Status: Accepted
Context: The app needs current, hourly, and daily forecast data plus city search — with no budget for API keys or billing risk.
Decision: Use Open-Meteo for both forecast (via the openmeteo SDK) and geocoding search.
Why: Free, reliable, keyless. No secret management, no usage caps to worry about.
Alternatives: OpenWeatherMap / WeatherAPI (require keys), scraping (fragile, against terms).
Trade-off: The SDK exposes variables by index, not name — and daily min/max encode identically, so named lookups are impossible. Handled with a documented index contract in the service layer.
ADR-002: Split Server State and UI State
Status: Accepted
Context: Fetched weather data and small client preferences have different lifetimes and rules.
Decision: TanStack Query owns server data, cached and deduplicated by coordinates; Zustand owns location, preferences (persisted), and panel visibility.
Why: Query brings caching, deduplication, and retries for data that must stay fresh; Zustand gives instant, granular subscriptions for state that must feel immediate.
Alternatives: Everything in Zustand (loses caching/dedup) or everything in Query (mixes transient UI state into a data cache).
Trade-off: Two libraries to reason about. In return: unit changes re-render only subscribers; weather data is reused across visits to the same coordinates.
ADR-003: Server-Component Shell with Client Islands
Status: Accepted
Context: The app should paint immediately and only hydrate what needs interactivity.
Decision: app/page.tsx is a React Server Component rendering the static shell; only Search and ForecastContainer are "use client" islands.
Why: First paint ships as static HTML while the data-heavy areas load independently on the client.
Alternatives: Fully client-side page (heavier first paint); fully static (can't show live weather).
Trade-off: Data fetching lives in client components by design; island boundaries must be kept deliberate.
ADR-004: Always-Mounted Skeleton Containers
Status: Accepted
Context: The naive loading approach — swap a small box for the whole grid — caused layout shift and one blocking render of ~40 widgets and ~30 images.
Decision: The forecast grid always renders. Each panel shows a skeleton shaped like its final content and fills in place when its slice of data arrives.
Why: Layout is reserved from first paint; there is no jump and no single blocking swap.
Alternatives: One global "Loading…" box (caused CLS); per-section lazy mounting (more orchestration complexity for little gain).
Trade-off: Slightly more JSX per container to mirror the empty layout.
ADR-005: Reverse Geocoding Behind a Server Action
Status: Accepted
Context: Turning coordinates into a readable city name requires an API that needs a key.
Decision: Call BigDataCloud from a "use server" server action; the key lives only in a server environment variable.
Why: The key never reaches the client bundle, yet the call stays a simple function from the client's perspective.
Alternatives: Direct client call (leaks the key); a custom proxy backend (out of scope for a frontend-only project).
Trade-off: The request hops through the Next.js server, so deployment requires a server runtime (Vercel covers this).
ADR-006: Co-Located CSS Modules with Design Tokens
Status: Accepted
Context: Consistent theming across many components with zero runtime styling cost.
Decision: Each component styles itself with a co-located CSS Module, driven by tokens (palette, spacing, breakpoints) defined once in globals.css. Responsive type scales via the root font size instead of per-component media queries.
Why: Build-time scoping with no runtime overhead; tokens keep the visual language consistent; root-font scaling makes every rem-based size responsive proportionally.
Alternatives: Tailwind (extra build dependency, verbose markup); global stylesheets (collision risk).
Trade-off: Breakpoint values must repeat literally inside component CSS because @media can't read var() — kept in sync via a token comment block.
Common thread
Every decision optimizes for perceived speed and honesty: paint fast, never shift, never refetch what you already have, and never leak a secret.