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()
};
}
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 };
}
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' };
}
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.'
};
}
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'
};
}
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);
});
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);
}
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 };
}
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)