How to Build a Weather App with OpenWeatherMap
·APIScout Team
openweathermapweather apireacttutorialapi integration
How to Build a Weather App with OpenWeatherMap
OpenWeatherMap provides weather data for any location on Earth. This guide builds a complete weather app: current conditions, 5-day forecast, location search, and weather alerts.
What You'll Build
- Current weather for any city
- 5-day / 3-hour forecast
- Location search with geocoding
- Weather icons and condition display
- Temperature unit toggle (°C / °F)
Prerequisites: React/Next.js, OpenWeatherMap account (free: 60 calls/minute, 1M calls/month).
1. Setup
Get API Key
- Sign up at openweathermap.org
- Go to API Keys → Generate key
- Wait ~10 minutes for key activation
NEXT_PUBLIC_OPENWEATHER_API_KEY=your_api_key
API Wrapper
// lib/weather.ts
const API_KEY = process.env.NEXT_PUBLIC_OPENWEATHER_API_KEY;
const BASE_URL = 'https://api.openweathermap.org';
export interface WeatherData {
name: string;
temp: number;
feelsLike: number;
humidity: number;
windSpeed: number;
description: string;
icon: string;
tempMin: number;
tempMax: number;
}
export async function getCurrentWeather(
lat: number,
lon: number,
units: 'metric' | 'imperial' = 'metric'
): Promise<WeatherData> {
const res = await fetch(
`${BASE_URL}/data/2.5/weather?lat=${lat}&lon=${lon}&units=${units}&appid=${API_KEY}`
);
const data = await res.json();
return {
name: data.name,
temp: Math.round(data.main.temp),
feelsLike: Math.round(data.main.feels_like),
humidity: data.main.humidity,
windSpeed: Math.round(data.wind.speed),
description: data.weather[0].description,
icon: data.weather[0].icon,
tempMin: Math.round(data.main.temp_min),
tempMax: Math.round(data.main.temp_max),
};
}
2. Geocoding (City Search)
// lib/weather.ts (continued)
export interface GeoLocation {
name: string;
country: string;
state?: string;
lat: number;
lon: number;
}
export async function searchCity(query: string): Promise<GeoLocation[]> {
const res = await fetch(
`${BASE_URL}/geo/1.0/direct?q=${encodeURIComponent(query)}&limit=5&appid=${API_KEY}`
);
const data = await res.json();
return data.map((loc: any) => ({
name: loc.name,
country: loc.country,
state: loc.state,
lat: loc.lat,
lon: loc.lon,
}));
}
Search Component
'use client';
import { useState } from 'react';
import { searchCity, GeoLocation } from '@/lib/weather';
export function CitySearch({ onSelect }: {
onSelect: (location: GeoLocation) => void;
}) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<GeoLocation[]>([]);
const handleSearch = async () => {
if (query.length < 2) return;
const cities = await searchCity(query);
setResults(cities);
};
return (
<div>
<div className="search-bar">
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="Search city..."
/>
<button onClick={handleSearch}>Search</button>
</div>
{results.length > 0 && (
<ul className="results">
{results.map((loc, i) => (
<li key={i} onClick={() => { onSelect(loc); setResults([]); }}>
{loc.name}, {loc.state && `${loc.state}, `}{loc.country}
</li>
))}
</ul>
)}
</div>
);
}
3. 5-Day Forecast
// lib/weather.ts (continued)
export interface ForecastItem {
dt: number;
date: string;
time: string;
temp: number;
description: string;
icon: string;
pop: number; // Probability of precipitation (0-1)
}
export async function getForecast(
lat: number,
lon: number,
units: 'metric' | 'imperial' = 'metric'
): Promise<ForecastItem[]> {
const res = await fetch(
`${BASE_URL}/data/2.5/forecast?lat=${lat}&lon=${lon}&units=${units}&appid=${API_KEY}`
);
const data = await res.json();
return data.list.map((item: any) => ({
dt: item.dt,
date: new Date(item.dt * 1000).toLocaleDateString(),
time: new Date(item.dt * 1000).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
temp: Math.round(item.main.temp),
description: item.weather[0].description,
icon: item.weather[0].icon,
pop: Math.round(item.pop * 100),
}));
}
Forecast Display
export function ForecastDisplay({ forecast }: { forecast: ForecastItem[] }) {
// Group by date
const grouped = forecast.reduce((acc, item) => {
if (!acc[item.date]) acc[item.date] = [];
acc[item.date].push(item);
return acc;
}, {} as Record<string, ForecastItem[]>);
return (
<div className="forecast">
{Object.entries(grouped).slice(0, 5).map(([date, items]) => {
const temps = items.map(i => i.temp);
const high = Math.max(...temps);
const low = Math.min(...temps);
return (
<div key={date} className="forecast-day">
<p className="date">{date}</p>
<img
src={`https://openweathermap.org/img/wn/${items[4]?.icon || items[0].icon}@2x.png`}
alt={items[0].description}
/>
<p>{high}° / {low}°</p>
<p className="description">{items[4]?.description || items[0].description}</p>
</div>
);
})}
</div>
);
}
4. Complete Weather App
'use client';
import { useState, useEffect } from 'react';
import { getCurrentWeather, getForecast, WeatherData, ForecastItem, GeoLocation } from '@/lib/weather';
import { CitySearch } from '@/components/CitySearch';
import { ForecastDisplay } from '@/components/ForecastDisplay';
export default function WeatherApp() {
const [weather, setWeather] = useState<WeatherData | null>(null);
const [forecast, setForecast] = useState<ForecastItem[]>([]);
const [units, setUnits] = useState<'metric' | 'imperial'>('metric');
const [location, setLocation] = useState<GeoLocation | null>(null);
const loadWeather = async (lat: number, lon: number) => {
const [w, f] = await Promise.all([
getCurrentWeather(lat, lon, units),
getForecast(lat, lon, units),
]);
setWeather(w);
setForecast(f);
};
const handleCitySelect = (loc: GeoLocation) => {
setLocation(loc);
loadWeather(loc.lat, loc.lon);
};
// Load user's location on mount
useEffect(() => {
navigator.geolocation.getCurrentPosition(
(pos) => loadWeather(pos.coords.latitude, pos.coords.longitude),
() => loadWeather(40.7128, -74.0060) // Default: New York
);
}, [units]);
const unitSymbol = units === 'metric' ? '°C' : '°F';
return (
<div className="weather-app">
<CitySearch onSelect={handleCitySelect} />
<button onClick={() => setUnits(units === 'metric' ? 'imperial' : 'metric')}>
Switch to {units === 'metric' ? '°F' : '°C'}
</button>
{weather && (
<div className="current-weather">
<h2>{weather.name}</h2>
<img
src={`https://openweathermap.org/img/wn/${weather.icon}@4x.png`}
alt={weather.description}
/>
<p className="temp">{weather.temp}{unitSymbol}</p>
<p className="description">{weather.description}</p>
<div className="details">
<p>Feels like: {weather.feelsLike}{unitSymbol}</p>
<p>H: {weather.tempMax}{unitSymbol} L: {weather.tempMin}{unitSymbol}</p>
<p>Humidity: {weather.humidity}%</p>
<p>Wind: {weather.windSpeed} {units === 'metric' ? 'm/s' : 'mph'}</p>
</div>
</div>
)}
{forecast.length > 0 && <ForecastDisplay forecast={forecast} />}
</div>
);
}
API Endpoints
| Endpoint | Free Tier | Data |
|---|---|---|
| Current Weather | ✅ | Temperature, humidity, wind, conditions |
| 5-Day Forecast | ✅ | 3-hour intervals for 5 days |
| Geocoding | ✅ | City name → coordinates |
| Weather Icons | ✅ | Condition-based icons |
| One Call 3.0 | 1,000 calls/day free | Current + hourly + daily + alerts |
| Historical | Paid only | Past weather data |
Pricing
| Plan | Calls/Month | Price |
|---|---|---|
| Free | 1,000,000 | $0 |
| Startup | 10,000/day | From $0 (One Call) |
| Professional | Custom | Custom |
Common Mistakes
| Mistake | Impact | Fix |
|---|---|---|
| Using city name instead of coordinates | Ambiguous results (Paris, TX vs Paris, France) | Use geocoding API → lat/lon → weather |
| Not caching responses | Hitting rate limits | Cache for 10-30 minutes |
| Forgetting units parameter | Default is Kelvin | Always specify units=metric or imperial |
| API key in client-side code | Key exposed | Use server-side API route as proxy |
| Not handling API errors | App crashes on bad response | Check response status before parsing |
Building with weather data? Compare OpenWeatherMap vs WeatherAPI vs Tomorrow.io on APIScout — free tiers, accuracy, and API design.