Ahmed Abdelaziz

Weather Now — Real-Time Weather Web AppDesign Docs

Architecture Decisions

Six key choices and why they were made.

Last updated August 2026

Overview

Six decisions that shaped Weather Now. Each has context, choice, why, and trade-off — no jargon wall.


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.