Skip to main content

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

  1. Sign up at openweathermap.org
  2. Go to API Keys → Generate key
  3. 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),
  };
}
// 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

EndpointFree TierData
Current WeatherTemperature, humidity, wind, conditions
5-Day Forecast3-hour intervals for 5 days
GeocodingCity name → coordinates
Weather IconsCondition-based icons
One Call 3.01,000 calls/day freeCurrent + hourly + daily + alerts
HistoricalPaid onlyPast weather data

Pricing

PlanCalls/MonthPrice
Free1,000,000$0
Startup10,000/dayFrom $0 (One Call)
ProfessionalCustomCustom

Common Mistakes

MistakeImpactFix
Using city name instead of coordinatesAmbiguous results (Paris, TX vs Paris, France)Use geocoding API → lat/lon → weather
Not caching responsesHitting rate limitsCache for 10-30 minutes
Forgetting units parameterDefault is KelvinAlways specify units=metric or imperial
API key in client-side codeKey exposedUse server-side API route as proxy
Not handling API errorsApp crashes on bad responseCheck response status before parsing

Building with weather data? Compare OpenWeatherMap vs WeatherAPI vs Tomorrow.io on APIScout — free tiers, accuracy, and API design.

Comments