DEV Community

Cover image for Weather APIs: Build a Dashboard That's Actually Useful
APIVerve
APIVerve

Posted on • Originally published at blog.apiverve.com

Weather APIs: Build a Dashboard That's Actually Useful

My first weather app was a disaster.

It fetched data on every page load. No caching. Three seconds of spinner, then the same temperature that hadn't changed since the last refresh. Users clicked away before the data even loaded.

Then I made it worse: I added a 5-day forecast, hourly breakdown, and three different weather icons. The page made 8 API calls before anything displayed. On mobile, it was basically unusable.

Weather features seem simple until you build them. Here's what I learned about doing it right.

Start Simple: Current Conditions

Before you build a full weather dashboard, get current conditions working well:

async function getCurrentWeather(city) {
  const res = await fetch(
    `https://api.apiverve.com/v1/weatherforecast?city=${encodeURIComponent(city)}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return {
    location: data.location,
    temperature: {
      current: data.current.temp,
      feelsLike: data.current.feelsLike,
      unit: 'celsius' // or fahrenheit based on user preference
    },
    conditions: data.current.description,
    humidity: data.current.humidity,
    wind: {
      speed: data.current.windSpeed,
      direction: data.current.windDirection
    },
    icon: data.current.icon,
    updatedAt: new Date().toISOString()
  };
}
Enter fullscreen mode Exit fullscreen mode

That's all you need for a widget. Location, temperature, conditions, and an icon. Everything else is decoration until this works.

The Caching Problem

Weather doesn't change every second. But if your app fetches fresh data on every request, you're wasting credits and making users wait.

const weatherCache = new Map();

async function getCachedWeather(city) {
  const cacheKey = city.toLowerCase().trim();
  const cached = weatherCache.get(cacheKey);

  // Cache for 15 minutes
  if (cached && Date.now() - cached.timestamp < 900000) {
    return { ...cached.data, fromCache: true };
  }

  const fresh = await getCurrentWeather(city);

  weatherCache.set(cacheKey, {
    data: fresh,
    timestamp: Date.now()
  });

  return { ...fresh, fromCache: false };
}
Enter fullscreen mode Exit fullscreen mode

15 minutes is a good default. Short enough to catch weather changes, long enough to avoid unnecessary calls. For a weather dashboard that refreshes automatically, this reduces API calls by 95% or more.

Temperature Preferences

Half your users want Celsius. Half want Fahrenheit. Let them choose.

function formatTemperature(celsius, unit = 'celsius') {
  if (unit === 'fahrenheit') {
    const fahrenheit = (celsius * 9/5) + 32;
    return `${Math.round(fahrenheit)}°F`;
  }
  return `${Math.round(celsius)}°C`;
}

function formatWindSpeed(kmh, unit = 'metric') {
  if (unit === 'imperial') {
    const mph = kmh * 0.621371;
    return `${Math.round(mph)} mph`;
  }
  return `${Math.round(kmh)} km/h`;
}

// Store preference
function setUnitPreference(units) {
  localStorage.setItem('weatherUnits', JSON.stringify(units));
}

function getUnitPreference() {
  const stored = localStorage.getItem('weatherUnits');
  return stored ? JSON.parse(stored) : { temperature: 'celsius', speed: 'metric' };
}
Enter fullscreen mode Exit fullscreen mode

Never assume. Ask once, remember forever.

Adding Air Quality

Weather alone is incomplete for many use cases. Air quality matters for:

  • Outdoor exercise recommendations
  • Allergy sufferers
  • Sensitive groups (elderly, children)
  • Event planning
async function getAirQuality(city) {
  const res = await fetch(
    `https://api.apiverve.com/v1/airquality?city=${encodeURIComponent(city)}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  // Interpret AQI (Air Quality Index)
  const aqiLevel = interpretAQI(data.aqi);

  return {
    aqi: data.aqi,
    level: aqiLevel.level,
    description: aqiLevel.description,
    recommendation: aqiLevel.recommendation,
    pollutants: data.pollutants
  };
}

function interpretAQI(aqi) {
  if (aqi <= 50) return {
    level: 'good',
    description: 'Air quality is satisfactory',
    recommendation: 'Great day for outdoor activities!'
  };
  if (aqi <= 100) return {
    level: 'moderate',
    description: 'Air quality is acceptable',
    recommendation: 'Unusually sensitive people should limit prolonged outdoor exertion.'
  };
  if (aqi <= 150) return {
    level: 'unhealthy-sensitive',
    description: 'Unhealthy for sensitive groups',
    recommendation: 'People with respiratory issues should limit outdoor exertion.'
  };
  if (aqi <= 200) return {
    level: 'unhealthy',
    description: 'Unhealthy',
    recommendation: 'Everyone should limit prolonged outdoor exertion.'
  };
  return {
    level: 'hazardous',
    description: 'Very unhealthy to hazardous',
    recommendation: 'Avoid outdoor activities. Keep windows closed.'
  };
}
Enter fullscreen mode Exit fullscreen mode

Now you can show users whether it's actually a good idea to go for that run.

Sunrise and Sunset

For outdoor apps, photography apps, or anyone who cares about daylight:

async function getDaylightInfo(lat, lon) {
  const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD

  const res = await fetch(
    `https://api.apiverve.com/v1/sunrisesunset?lat=${lat}&lon=${lon}&date=${today}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  const now = new Date();
  const sunrise = new Date(data.sunrise);
  const sunset = new Date(data.sunset);

  // Calculate golden hour (roughly 1 hour after sunrise, 1 hour before sunset)
  const morningGoldenStart = new Date(sunrise.getTime());
  const morningGoldenEnd = new Date(sunrise.getTime() + 3600000);
  const eveningGoldenStart = new Date(sunset.getTime() - 3600000);
  const eveningGoldenEnd = new Date(sunset.getTime());

  return {
    sunrise: data.sunrise,
    sunset: data.sunset,
    dayLength: data.dayLength,
    isDay: now > sunrise && now < sunset,
    goldenHours: {
      morning: { start: morningGoldenStart, end: morningGoldenEnd },
      evening: { start: eveningGoldenStart, end: eveningGoldenEnd }
    },
    nextGoldenHour: now < morningGoldenEnd ? 'morning' : 'evening'
  };
}
Enter fullscreen mode Exit fullscreen mode

Photographers will love the golden hour feature. Event planners use sunset times for outdoor venues.

Combining It All

Here's a complete weather dashboard service:

class WeatherDashboard {
  constructor() {
    this.cache = new Map();
    this.cacheTimeout = 900000; // 15 minutes
  }

  async getFullWeather(location) {
    const cacheKey = `full:${location.city || `${location.lat},${location.lon}`}`;
    const cached = this.cache.get(cacheKey);

    if (cached && Date.now() - cached.timestamp < this.cacheTimeout) {
      return { ...cached.data, fromCache: true };
    }

    // Fetch all data in parallel
    const [weather, airQuality, daylight] = await Promise.all([
      this.fetchWeather(location),
      this.fetchAirQuality(location),
      location.lat && location.lon
        ? this.fetchDaylight(location.lat, location.lon)
        : null
    ]);

    const combined = {
      weather,
      airQuality,
      daylight,
      fetchedAt: new Date().toISOString(),
      location
    };

    this.cache.set(cacheKey, {
      data: combined,
      timestamp: Date.now()
    });

    return combined;
  }

  async fetchWeather(location) {
    const query = location.city
      ? `city=${encodeURIComponent(location.city)}`
      : `lat=${location.lat}&lon=${location.lon}`;

    const res = await fetch(
      `https://api.apiverve.com/v1/weatherforecast?${query}`,
      { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
    );
    return (await res.json()).data;
  }

  async fetchAirQuality(location) {
    const query = location.city
      ? `city=${encodeURIComponent(location.city)}`
      : `lat=${location.lat}&lon=${location.lon}`;

    try {
      const res = await fetch(
        `https://api.apiverve.com/v1/airquality?${query}`,
        { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
      );
      return (await res.json()).data;
    } catch {
      return null; // Air quality optional, don't fail if unavailable
    }
  }

  async fetchDaylight(lat, lon) {
    const today = new Date().toISOString().split('T')[0];

    const res = await fetch(
      `https://api.apiverve.com/v1/sunrisesunset?lat=${lat}&lon=${lon}&date=${today}`,
      { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
    );
    return (await res.json()).data;
  }
}

// Usage
const dashboard = new WeatherDashboard();

app.get('/api/weather', async (req, res) => {
  const { city, lat, lon } = req.query;

  const location = city
    ? { city }
    : { lat: parseFloat(lat), lon: parseFloat(lon) };

  const data = await dashboard.getFullWeather(location);
  res.json(data);
});
Enter fullscreen mode Exit fullscreen mode

One API call from your frontend. Three parallel fetches behind the scenes. All cached together.

Auto-Detecting Location

Asking users to type their city is friction. Use IP geolocation or browser location:

// Option 1: IP-based (no permission needed, less accurate)
async function getLocationFromIP(ip) {
  const res = await fetch(
    `https://api.apiverve.com/v1/iplookup?ip=${ip}`,
    { headers: { 'x-api-key': process.env.APIVERVE_KEY } }
  );
  const { data } = await res.json();

  return {
    city: data.city,
    lat: data.coordinates?.latitude,
    lon: data.coordinates?.longitude,
    method: 'ip'
  };
}

// Option 2: Browser geolocation (permission required, very accurate)
function getBrowserLocation() {
  return new Promise((resolve, reject) => {
    if (!navigator.geolocation) {
      reject(new Error('Geolocation not supported'));
      return;
    }

    navigator.geolocation.getCurrentPosition(
      position => resolve({
        lat: position.coords.latitude,
        lon: position.coords.longitude,
        method: 'gps'
      }),
      error => reject(error),
      { timeout: 10000, maximumAge: 600000 }
    );
  });
}

// Combined approach
async function detectLocation(req) {
  // Try browser location first (if frontend has it)
  if (req.query.lat && req.query.lon) {
    return {
      lat: parseFloat(req.query.lat),
      lon: parseFloat(req.query.lon),
      method: 'gps'
    };
  }

  // Fall back to IP-based
  const ip = req.headers['x-forwarded-for']?.split(',')[0] || req.socket.remoteAddress;
  return getLocationFromIP(ip);
}
Enter fullscreen mode Exit fullscreen mode

Best UX: try browser location, fall back to IP, let user manually override.

Refreshing Intelligently

Don't poll every 5 seconds. Weather doesn't change that fast. But do refresh when:

// React hook example
function useWeather(location) {
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(true);

  const fetchWeather = useCallback(async () => {
    setLoading(true);
    const data = await dashboard.getFullWeather(location);
    setWeather(data);
    setLoading(false);
  }, [location]);

  // Initial fetch
  useEffect(() => {
    fetchWeather();
  }, [fetchWeather]);

  // Refresh every 15 minutes
  useEffect(() => {
    const interval = setInterval(fetchWeather, 900000);
    return () => clearInterval(interval);
  }, [fetchWeather]);

  // Refresh when tab becomes visible again
  useEffect(() => {
    const handleVisibility = () => {
      if (document.visibilityState === 'visible') {
        fetchWeather();
      }
    };

    document.addEventListener('visibilitychange', handleVisibility);
    return () => document.removeEventListener('visibilitychange', handleVisibility);
  }, [fetchWeather]);

  return { weather, loading, refresh: fetchWeather };
}
Enter fullscreen mode Exit fullscreen mode

The visibility change handler is key. If someone leaves your tab open for hours and comes back, they should see fresh data, not stale information from when they left.

Common Mistakes

Fetching on every page load. Use caching. Weather doesn't change every second.

Not handling location failures. Browser location can be denied, IP lookup can fail. Always have a fallback (manual city entry).

Showing raw API data. "Partly cloudy" is better than "partly-cloudy". "72°F" is better than "22.2222°C". Format for humans.

Forgetting timezones. Sunrise at 6:32am UTC is meaningless if the user is in Tokyo. Convert to local time.

Treating air quality as required. If AQ data isn't available, show weather without it. Don't fail completely because one optional feature is unavailable.


Weather features seem simple because we use them every day. Building them well requires thinking about caching, location detection, unit preferences, and graceful degradation.

Start with current conditions. Get that working smoothly. Then add forecasts, air quality, and sunrise/sunset. Each piece is simple; the complexity is in combining them intelligently.

The Weather Forecast, Air Quality, and Sunrise/Sunset APIs all work together. Same authentication, consistent response formats.

Get your API key and build weather features that users actually appreciate.


Originally published at APIVerve Blog

Top comments (0)