How to Build a Weather App with OpenWeatherMap 2026
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.
TL;DR
OpenWeatherMap's free tier (1M calls/month) is generous enough for any hobby or production app at reasonable traffic levels. The One Call API 3.0 gives you current conditions, hourly forecasts, daily forecasts, and weather alerts all in a single request — use it instead of juggling multiple endpoints once your usage justifies the upgrade. The main limitation to know upfront: historical data requires a paid plan. For new projects where forecast accuracy is critical, also evaluate Tomorrow.io vs OpenWeatherMap before committing — Tomorrow.io's ML-based models perform measurably better for hyperlocal short-range forecasts.
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).
OpenWeatherMap's Position and When to Consider Alternatives
OpenWeatherMap has been collecting and distributing weather data for over 15 years and now serves more than 2 million registered developers. The breadth of its free tier is unmatched among mainstream weather APIs — 1M calls per month with no credit card required is a genuine no-strings offering that makes it the default starting point for weather app tutorials, hackathons, and early-stage products. The data quality for current conditions and multi-day forecasts is solid for most use cases, drawing from a network of global weather stations, satellite data, and numerical weather prediction models.
That said, OpenWeatherMap is not always the right choice. For applications where hyperlocal accuracy matters — agricultural tools, outdoor event planning, precision sports applications — Tomorrow.io uses machine learning on radar data and achieves meaningfully better short-range forecast accuracy, particularly for precipitation timing. WeatherAPI offers higher rate limits on its free tier if you need more headroom without paying. And if you need historical data going back years, OpenWeatherMap's paid plans are the only path forward. For a full comparison of the options, the best weather and climate APIs for 2026 covers pricing, accuracy, and API design across the major providers.
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)
Passing a city name string directly to a weather endpoint is a footgun that trips up many developers. "Paris" resolves differently depending on the API — you might get Paris, France or Paris, Texas. "Springfield" in the United States alone has over thirty instances. "Birmingham" exists in both Alabama and England. The correct approach is always a two-step process: first resolve the location name to geographic coordinates using the Geocoding API, then pass those coordinates to the weather endpoint. This guarantees you're retrieving data for the location the user actually selected, and it enables your UI to show disambiguation when multiple matches exist for the same name.
// 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
The 5-day forecast endpoint returns 40 data points at 3-hour intervals, giving you a complete picture of conditions over the next five days. Each data point includes temperature, feels-like temperature, weather conditions, wind speed, and a precipitation probability value between 0 and 1. The raw response is a flat list ordered chronologically — to display a day-by-day summary card (the pattern most weather apps use), you need to group these data points by calendar date. The ForecastDisplay component below does this with a reduce call. One practical note: the icon used for each day's summary card matters — using items[4] selects the data point around midday rather than the earliest point of the day (which might be before sunrise), giving a more representative visual for the day's conditions.
// 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
The complete component brings together several UX decisions worth calling out explicitly. Browser geolocation is requested on mount as a convenience — if the user grants permission, the app loads their current location immediately without requiring a search. The error callback falls back to New York coordinates, which is a reasonable default for a demo but should be replaced with a user-facing prompt in production. The unit toggle re-triggers the useEffect by including units in the dependency array, which means switching between Celsius and Fahrenheit automatically reloads weather data in the correct unit. Using Promise.all to fetch current weather and forecast in parallel cuts the perceived load time roughly in half compared to sequential requests. Both calls share the same lat, lon, and units parameters, so there's no ordering dependency between them.
'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 |
Once your app is past the prototyping stage, One Call API 3.0 is worth evaluating as a replacement for the separate current weather and forecast endpoints used in this guide. A single One Call request returns current conditions, minute-by-minute precipitation for the next hour, hourly forecasts for 48 hours, daily forecasts for 8 days, and any active government weather alerts — all in one response. The free allocation of 1,000 calls per day is enough for modest traffic, and the pay-per-call model beyond that is predictable. Consolidating to One Call also reduces the number of API keys and base URLs you need to manage.
Pricing
| Plan | Calls/Month | Price |
|---|---|---|
| Free | 1,000,000 | $0 |
| Startup | 10,000/day | From $0 (One Call) |
| Professional | Custom | Custom |
With 1M free calls per month across the standard endpoints, rate limits are rarely a concern for individual developers or small teams. The situation changes at scale: a weather dashboard with 10,000 active users each loading the page twice daily would consume 20,000 calls per day on current conditions alone, before accounting for forecasts. The right mitigation is a server-side proxy with response caching — cache weather data for 10 to 30 minutes per location, serve cached responses to all users requesting the same location, and only hit the OpenWeatherMap API when the cache has expired. This pattern also keeps your API key out of the client bundle, which is the correct approach regardless of rate limit concerns. For a broader treatment of rate limit strategy across APIs, see our API rate limiting best practices guide.
Building a Production-Ready API Layer
The lib/weather.ts module in this guide exposes the OpenWeatherMap API directly from client-side React components via environment variables prefixed with NEXT_PUBLIC_. That pattern is fine for prototyping but has two problems in production. First, your API key is visible in the browser's network requests and in your compiled JavaScript bundle — anyone can extract it and burn through your quota. Second, you have no place to insert caching, rate limiting, or error handling between the client and the third-party API.
The production-correct architecture is a server-side proxy: create a Next.js API route (or a server action) that receives coordinates or a city query from the client, calls OpenWeatherMap using a server-only environment variable, caches the result, and returns it to the client. This approach keeps your API key server-side, allows you to implement response caching with a library like node-cache or via Redis, and gives you a single place to handle error cases uniformly.
For caching strategy: weather data at a given location does not change faster than every 10 minutes under any real-world forecast model. Caching responses for 15 to 30 minutes per unique (lat, lon, units) combination reduces your API call volume dramatically for any application with repeat visitors. A city of 100,000 people will have thousands of users checking weather for the same coordinates — caching turns those thousands of API calls into one per 30-minute window. This is not just an optimization; it is the difference between staying on the free tier and incurring unexpected costs as your app grows.
Error handling in the getCurrentWeather and getForecast functions above does no status checking — if OpenWeatherMap returns a 401 (invalid API key), 404 (location not found), or 429 (rate limited), the res.json() call returns an error object rather than the expected data structure, and the property access below it throws a confusing runtime error. In production, check res.ok before parsing and throw a typed error with the HTTP status so the caller can respond appropriately — showing a "location not found" message vs a "service unavailable" message are different user experiences.
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? See the best weather and climate APIs for 2026 and our Tomorrow.io vs OpenWeatherMap comparison — accuracy, free tiers, and API design.