Project Overview
Welcome to your first API-based project! Today, we're building a weather application that fetches real-time weather data. Think of it like creating your own mini weather channel. Users will enter a city name, and your app will display current weather conditions, temperature, humidity, and more. This project combines everything you've learned this week: DOM manipulation, event handling, asynchronous JavaScript, and error handling.
Project Requirements
- Create a user interface with an input field for city names
- Fetch weather data from OpenWeatherMap API
- Display current weather information including:
- City name
- Temperature (with ability to switch between Celsius/Fahrenheit)
- Weather description
- Humidity
- Wind speed
- Weather icon
- Handle errors gracefully (invalid city, network issues)
- Add loading indicators during API calls
- Make the design responsive
Using George Polya's Problem-Solving Method
Step 1: Understand the Problem
We need to create a weather application that:
- Takes user input (city name)
- Makes an API call to get weather data
- Displays the weather information
- Handles errors appropriately
- Provides a good user experience with loading states
Step 2: Devise a Plan
- Set up the project structure (HTML, CSS, JavaScript files)
- Create the HTML layout with necessary elements
- Style the application with CSS
- Get an API key from OpenWeatherMap
- Write JavaScript to:
- Handle form submission
- Make API requests
- Process and display data
- Handle errors
- Add temperature unit conversion
- Test with different cities and error scenarios
- Add final touches and improvements
Step 3: Carry Out the Plan
Let's implement our weather application step by step.
Step 4: Look Back and Reflect
After completing the project, we'll review what we've learned and consider improvements.
Project Structure
graph TD
A[weather-app/] --> B[index.html]
A --> C[styles/]
A --> D[js/]
C --> E[style.css]
D --> F[app.js]
D --> G[config.js]
A --> H[images/]
H --> I[weather-icons/]
Step 1: Create the HTML Structure
File: /weather-app/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Weather App</title>
<link rel="stylesheet" href="styles/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
</head>
<body>
<div class="container">
<header class="header">
<h1><i class="fas fa-cloud-sun"></i> Weather App</h1>
</header>
<main class="main">
<div class="search-box">
<form id="searchForm">
<input
type="text"
id="cityInput"
placeholder="Enter city name..."
autocomplete="off"
required
>
<button type="submit" id="searchBtn">
<i class="fas fa-search"></i>
</button>
</form>
</div>
<div class="loading" id="loading" style="display: none;">
<i class="fas fa-spinner fa-spin"></i>
<p>Loading weather data...</p>
</div>
<div class="error" id="error" style="display: none;">
<i class="fas fa-exclamation-circle"></i>
<p id="errorMessage"></p>
</div>
<div class="weather-card" id="weatherCard" style="display: none;">
<div class="weather-header">
<h2 id="cityName"></h2>
<p id="dateTime"></p>
</div>
<div class="weather-body">
<div class="temperature">
<img id="weatherIcon" src="" alt="Weather icon">
<div class="temp-main">
<span id="temperature"></span>
<div class="temp-toggle">
<button class="active" id="celsiusBtn">°C</button>
<button id="fahrenheitBtn">°F</button>
</div>
</div>
</div>
<div class="weather-description">
<p id="description"></p>
<p id="feelsLike">Feels like: <span></span></p>
</div>
</div>
<div class="weather-details">
<div class="detail">
<i class="fas fa-tint"></i>
<p>Humidity</p>
<p id="humidity"></p>
</div>
<div class="detail">
<i class="fas fa-wind"></i>
<p>Wind Speed</p>
<p id="windSpeed"></p>
</div>
<div class="detail">
<i class="fas fa-compress-arrows-alt"></i>
<p>Pressure</p>
<p id="pressure"></p>
</div>
</div>
</div>
</main>
<footer class="footer">
<p>Data provided by OpenWeatherMap</p>
</footer>
</div>
<script src="js/config.js"></script>
<script src="js/app.js"></script>
</body>
</html>
Step 2: Create the CSS Styles
File: /weather-app/styles/style.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #74ebd5 0%, #acb6e5 100%);
min-height: 100vh;
color: #333;
}
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
color: white;
}
.header h1 {
font-size: 2.5rem;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.search-box {
margin-bottom: 30px;
}
#searchForm {
display: flex;
max-width: 500px;
margin: 0 auto;
}
#cityInput {
flex: 1;
padding: 15px;
font-size: 1.1rem;
border: none;
border-radius: 25px 0 0 25px;
outline: none;
}
#searchBtn {
padding: 15px 25px;
font-size: 1.1rem;
background: #4a90e2;
color: white;
border: none;
border-radius: 0 25px 25px 0;
cursor: pointer;
transition: background 0.3s;
}
#searchBtn:hover {
background: #357abd;
}
.loading, .error {
text-align: center;
padding: 20px;
margin: 20px auto;
max-width: 500px;
border-radius: 10px;
}
.loading {
background: rgba(255, 255, 255, 0.9);
}
.error {
background: #ffebee;
color: #c62828;
}
.weather-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
padding: 30px;
max-width: 600px;
margin: 0 auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
}
.weather-header {
text-align: center;
margin-bottom: 20px;
}
.weather-header h2 {
font-size: 2rem;
margin-bottom: 5px;
}
.temperature {
display: flex;
align-items: center;
justify-content: center;
margin: 20px 0;
}
.temp-main {
display: flex;
align-items: center;
gap: 10px;
}
#temperature {
font-size: 4rem;
font-weight: bold;
}
.temp-toggle {
display: flex;
flex-direction: column;
gap: 5px;
}
.temp-toggle button {
padding: 5px 10px;
border: 1px solid #ddd;
background: white;
cursor: pointer;
border-radius: 5px;
}
.temp-toggle button.active {
background: #4a90e2;
color: white;
border-color: #4a90e2;
}
.weather-description {
text-align: center;
margin: 20px 0;
}
#description {
font-size: 1.5rem;
text-transform: capitalize;
margin-bottom: 10px;
}
.weather-details {
display: flex;
justify-content: space-around;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #eee;
}
.detail {
text-align: center;
}
.detail i {
font-size: 1.5rem;
color: #4a90e2;
margin-bottom: 10px;
}
.footer {
text-align: center;
margin-top: 50px;
color: white;
text-shadow: 1px 1px 2px rgba(0,0,0,0.2);
}
@media (max-width: 600px) {
.weather-details {
flex-direction: column;
gap: 20px;
}
#temperature {
font-size: 3rem;
}
.header h1 {
font-size: 2rem;
}
}
Step 3: Create the Configuration File
File: /weather-app/js/config.js
// OpenWeatherMap API configuration
const API_KEY = 'YOUR_API_KEY_HERE'; // Replace with your actual API key
const BASE_URL = 'https://api.openweathermap.org/data/2.5/weather';
// To get an API key:
// 1. Go to https://openweathermap.org/
// 2. Sign up for a free account
// 3. Go to API keys section in your account
// 4. Generate a new API key
// 5. Replace 'YOUR_API_KEY_HERE' with your actual key
Step 4: Create the Main JavaScript File
File: /weather-app/js/app.js
// DOM Elements
const searchForm = document.getElementById('searchForm');
const cityInput = document.getElementById('cityInput');
const weatherCard = document.getElementById('weatherCard');
const loading = document.getElementById('loading');
const error = document.getElementById('error');
const errorMessage = document.getElementById('errorMessage');
// Weather data elements
const cityName = document.getElementById('cityName');
const dateTime = document.getElementById('dateTime');
const temperature = document.getElementById('temperature');
const weatherIcon = document.getElementById('weatherIcon');
const description = document.getElementById('description');
const feelsLike = document.querySelector('#feelsLike span');
const humidity = document.getElementById('humidity');
const windSpeed = document.getElementById('windSpeed');
const pressure = document.getElementById('pressure');
// Temperature toggle buttons
const celsiusBtn = document.getElementById('celsiusBtn');
const fahrenheitBtn = document.getElementById('fahrenheitBtn');
// State
let currentWeatherData = null;
let isCelsius = true;
// Event Listeners
searchForm.addEventListener('submit', handleSearch);
celsiusBtn.addEventListener('click', () => toggleTemperatureUnit(true));
fahrenheitBtn.addEventListener('click', () => toggleTemperatureUnit(false));
// Handle search form submission
async function handleSearch(e) {
e.preventDefault();
const city = cityInput.value.trim();
if (!city) return;
// Reset UI
showLoading();
hideError();
hideWeatherCard();
try {
const data = await fetchWeatherData(city);
currentWeatherData = data;
displayWeatherData(data);
showWeatherCard();
} catch (err) {
showError(err.message);
} finally {
hideLoading();
}
}
// Fetch weather data from API
async function fetchWeatherData(city) {
const url = `${BASE_URL}?q=${encodeURIComponent(city)}&appid=${API_KEY}&units=metric`;
try {
const response = await fetch(url);
if (!response.ok) {
if (response.status === 404) {
throw new Error('City not found. Please check the spelling and try again.');
} else if (response.status === 401) {
throw new Error('Invalid API key. Please check your configuration.');
} else {
throw new Error('Failed to fetch weather data. Please try again later.');
}
}
const data = await response.json();
return data;
} catch (err) {
if (err.message.includes('Failed to fetch')) {
throw new Error('Network error. Please check your internet connection.');
}
throw err;
}
}
// Display weather data in the UI
function displayWeatherData(data) {
// Update city name and date
cityName.textContent = `${data.name}, ${data.sys.country}`;
dateTime.textContent = new Date().toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
// Update temperature
updateTemperatureDisplay(data.main.temp);
// Update weather icon
const iconCode = data.weather[0].icon;
weatherIcon.src = `https://openweathermap.org/img/wn/${iconCode}@2x.png`;
weatherIcon.alt = data.weather[0].description;
// Update description
description.textContent = data.weather[0].description;
// Update other details
updateFeelsLikeDisplay(data.main.feels_like);
humidity.textContent = `${data.main.humidity}%`;
windSpeed.textContent = `${data.wind.speed} m/s`;
pressure.textContent = `${data.main.pressure} hPa`;
}
// Temperature conversion functions
function celsiusToFahrenheit(celsius) {
return (celsius * 9/5) + 32;
}
function updateTemperatureDisplay(tempCelsius) {
if (isCelsius) {
temperature.textContent = `${Math.round(tempCelsius)}°C`;
} else {
temperature.textContent = `${Math.round(celsiusToFahrenheit(tempCelsius))}°F`;
}
}
function updateFeelsLikeDisplay(feelsLikeCelsius) {
if (isCelsius) {
feelsLike.textContent = `${Math.round(feelsLikeCelsius)}°C`;
} else {
feelsLike.textContent = `${Math.round(celsiusToFahrenheit(feelsLikeCelsius))}°F`;
}
}
// Toggle temperature unit
function toggleTemperatureUnit(toCelsius) {
if (isCelsius === toCelsius) return;
isCelsius = toCelsius;
// Update button states
if (isCelsius) {
celsiusBtn.classList.add('active');
fahrenheitBtn.classList.remove('active');
} else {
celsiusBtn.classList.remove('active');
fahrenheitBtn.classList.add('active');
}
// Update temperature displays if we have data
if (currentWeatherData) {
updateTemperatureDisplay(currentWeatherData.main.temp);
updateFeelsLikeDisplay(currentWeatherData.main.feels_like);
}
}
// UI state management functions
function showLoading() {
loading.style.display = 'block';
}
function hideLoading() {
loading.style.display = 'none';
}
function showError(message) {
errorMessage.textContent = message;
error.style.display = 'block';
}
function hideError() {
error.style.display = 'none';
}
function showWeatherCard() {
weatherCard.style.display = 'block';
}
function hideWeatherCard() {
weatherCard.style.display = 'none';
}
// Initialize the app
function init() {
// Clear input on page load
cityInput.value = '';
// Hide all cards initially
hideLoading();
hideError();
hideWeatherCard();
// Focus on input
cityInput.focus();
}
// Run initialization
init();
Application Flow Diagram
graph TD
A[User enters city name] --> B[Form submission]
B --> C[Show loading state]
C --> D[Fetch weather data from API]
D --> E{API Response}
E -->|Success| F[Parse JSON data]
E -->|Error| G[Show error message]
F --> H[Update UI with weather data]
H --> I[Hide loading state]
G --> I
I --> J[User can toggle temperature units]
J --> K[Update temperature display]
API Response Structure
// Example API response from OpenWeatherMap
{
"coord": { "lon": -0.1257, "lat": 51.5085 },
"weather": [
{
"id": 802,
"main": "Clouds",
"description": "scattered clouds",
"icon": "03d"
}
],
"base": "stations",
"main": {
"temp": 15.5,
"feels_like": 14.8,
"temp_min": 13.9,
"temp_max": 16.7,
"pressure": 1013,
"humidity": 67
},
"visibility": 10000,
"wind": {
"speed": 4.1,
"deg": 250
},
"clouds": { "all": 40 },
"dt": 1635701200,
"sys": {
"type": 2,
"id": 2019646,
"country": "GB",
"sunrise": 1635663371,
"sunset": 1635699467
},
"timezone": 0,
"id": 2643743,
"name": "London",
"cod": 200
}
Error Handling Scenarios
- City not found: API returns 404 status
- Invalid API key: API returns 401 status
- Network error: Fetch fails due to connection issues
- API limit exceeded: API returns 429 status
- Server error: API returns 5xx status
// Enhanced error handling
async function fetchWeatherData(city) {
try {
const response = await fetch(url);
if (!response.ok) {
switch (response.status) {
case 404:
throw new Error('City not found. Please check the spelling.');
case 401:
throw new Error('Invalid API key. Please check configuration.');
case 429:
throw new Error('Too many requests. Please try again later.');
case 500:
case 502:
case 503:
case 504:
throw new Error('Server error. Please try again later.');
default:
throw new Error('Failed to fetch weather data.');
}
}
return await response.json();
} catch (err) {
if (err instanceof TypeError && err.message.includes('Failed to fetch')) {
throw new Error('Network error. Check your internet connection.');
}
throw err;
}
}
Additional Features to Consider
- Geolocation: Get weather for user's current location
- Search history: Save recent searches using localStorage
- Multiple day forecast: Show 5-day forecast
- Weather maps: Integrate weather radar maps
- Favorite cities: Save and quickly access favorite locations
- Dark mode: Add theme switching capability
- Animations: Add weather-related animations
Implementing Geolocation Feature
// Add geolocation button to HTML
<button id="locationBtn" title="Use my location">
<i class="fas fa-location-arrow"></i>
</button>
// Add geolocation functionality
const locationBtn = document.getElementById('locationBtn');
locationBtn.addEventListener('click', getLocationWeather);
function getLocationWeather() {
if (!navigator.geolocation) {
showError('Geolocation is not supported by your browser');
return;
}
showLoading();
navigator.geolocation.getCurrentPosition(
async (position) => {
const { latitude, longitude } = position.coords;
try {
const data = await fetchWeatherByCoords(latitude, longitude);
currentWeatherData = data;
displayWeatherData(data);
showWeatherCard();
} catch (err) {
showError(err.message);
} finally {
hideLoading();
}
},
(error) => {
hideLoading();
switch(error.code) {
case error.PERMISSION_DENIED:
showError('Location permission denied');
break;
case error.POSITION_UNAVAILABLE:
showError('Location information unavailable');
break;
case error.TIMEOUT:
showError('Location request timed out');
break;
default:
showError('An unknown error occurred');
}
}
);
}
async function fetchWeatherByCoords(lat, lon) {
const url = `${BASE_URL}?lat=${lat}&lon=${lon}&appid=${API_KEY}&units=metric`;
const response = await fetch(url);
if (!response.ok) {
throw new Error('Failed to fetch weather data');
}
return await response.json();
}
Adding Local Storage for Search History
// Search history functions
function saveToHistory(city) {
let history = JSON.parse(localStorage.getItem('weatherSearchHistory') || '[]');
// Remove if already exists
history = history.filter(item => item.toLowerCase() !== city.toLowerCase());
// Add to beginning
history.unshift(city);
// Keep only last 5 searches
history = history.slice(0, 5);
localStorage.setItem('weatherSearchHistory', JSON.stringify(history));
updateHistoryDisplay();
}
function loadHistory() {
return JSON.parse(localStorage.getItem('weatherSearchHistory') || '[]');
}
function updateHistoryDisplay() {
const history = loadHistory();
const historyContainer = document.getElementById('searchHistory');
if (history.length === 0) {
historyContainer.style.display = 'none';
return;
}
historyContainer.style.display = 'block';
historyContainer.innerHTML = `
<h3>Recent Searches</h3>
<div class="history-list">
${history.map(city => `
<button class="history-item" data-city="${city}">
${city}
</button>
`).join('')}
</div>
`;
// Add click handlers
document.querySelectorAll('.history-item').forEach(button => {
button.addEventListener('click', () => {
cityInput.value = button.dataset.city;
searchForm.dispatchEvent(new Event('submit'));
});
});
}
Testing Your Application
Test Cases to Consider:
- Valid city names: London, New York, Tokyo
- Cities with spaces: Los Angeles, San Francisco
- Cities with special characters: São Paulo, Zürich
- Invalid city names: Xyzabc, 123456
- Empty input: Should not make API call
- Network offline: Should show appropriate error
- Temperature conversion: Celsius to Fahrenheit
- Responsive design: Test on different screen sizes
Deployment Considerations
- Secure API key: Never expose API key in client-side code for production
- Use environment variables: Store sensitive data securely
- Consider a backend proxy: Create a simple server to handle API calls
- HTTPS: Deploy on HTTPS for geolocation to work
- Error tracking: Implement error logging for production
- Performance optimization: Minify CSS and JavaScript
- Caching: Implement caching for API responses
Common Issues and Solutions
| Issue | Possible Cause | Solution |
|---|---|---|
| API returns 401 | Invalid API key | Check API key in config.js |
| CORS errors | Browser security policy | Use the CORS-enabled API endpoint |
| Weather icon not loading | HTTP/HTTPS mismatch | Use https:// for icon URLs |
| Geolocation not working | Not on HTTPS | Deploy on HTTPS or use localhost |
| Temperature not updating | State not managed correctly | Check currentWeatherData state |
Best Practices Applied
- Separation of concerns: HTML, CSS, and JavaScript are separated
- Error handling: Comprehensive error handling for better UX
- Loading states: Visual feedback during API calls
- Responsive design: Works on all device sizes
- Accessibility: Semantic HTML and ARIA attributes
- Code organization: Functions are focused and reusable
- State management: Clear state handling for temperature units
- User feedback: Clear messages for all interactions
Key Learning Outcomes
- Working with external APIs and handling API keys
- Implementing async/await for API calls
- Error handling and user feedback
- DOM manipulation and event handling
- State management in vanilla JavaScript
- Responsive design principles
- Working with browser APIs (Geolocation)
- Local storage for data persistence
Next Steps and Enhancements
- Add hourly forecast display
- Implement weather alerts and notifications
- Add weather-based background images
- Create a mobile app version using PWA
- Add weather charts using Chart.js
- Implement autocomplete for city search
- Add multiple language support
- Create weather widgets for different platforms