1. The Problem
Build a command-line weather tool: node weather.mjs Mumbai fetches
current conditions from the OpenWeatherMap API and prints a clean summary.
Handle errors gracefully (city not found, network failure, missing API key).
Add a retry mechanism for transient failures.
Why this project? This is the first project to use a real external API — the pattern you'll use in almost every real-world JavaScript program. async/await, fetch, error handling, environment variables, and command-line arguments all come together.
2. How to Think About This
Separate concerns into three layers:
- API layer: fetchWeather(city) — makes the HTTP request, throws on error
- Formatting layer: formatWeather(data) — pure function, converts API data to display string
- CLI layer: reads process.argv and the API key from process.env, orchestrates the other two
The formatWeather function is a pure function — testable without any network. The API key must come from an environment variable (never hardcode secrets).
3. The Build
// ── Formatting (pure — no I/O) ────────────────────────────────────────────
export function formatWeather(data, units = "metric") {
const { name, sys, main, weather, wind, clouds } = data;
const desc = weather[0]?.description ?? "unknown";
const unit = units === "metric" ? "°C" : "°F";
const temp = units === "metric"
? (main.temp - 273.15).toFixed(1)
: ((main.temp - 273.15) * 9/5 + 32).toFixed(1);
const feels = units === "metric"
? (main.feels_like - 273.15).toFixed(1)
: ((main.feels_like - 273.15) * 9/5 + 32).toFixed(1);
const windKph = (wind.speed * 3.6).toFixed(1);
return [
`
📍 ${name}, ${sys?.country ?? ""}`,
` ${desc.charAt(0).toUpperCase() + desc.slice(1)}`,
` 🌡 ${temp}${unit} (feels like ${feels}${unit})`,
` 💧 Humidity: ${main.humidity}%`,
` 💨 Wind: ${windKph} km/h`,
` ☁️ Cloud cover: ${clouds?.all ?? "?"}%`,
` 🌅 Sunrise: ${new Date(sys.sunrise * 1000).toLocaleTimeString()}`,
` 🌇 Sunset: ${new Date(sys.sunset * 1000).toLocaleTimeString()}`,
].join("
");
}
// ── API layer ──────────────────────────────────────────────────────────────
export async function fetchWeather(city, apiKey) {
const url = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&appid=${apiKey}`;
const res = await fetch(url);
const data = await res.json(); // read body even on error (has error message)
if (!res.ok) {
const msg = data.message ?? `HTTP ${res.status}`;
throw new Error(msg.includes("city not found")
? `City "${city}" not found. Check the spelling.`
: `API error: ${msg}`);
}
return data;
}
// ── Retry with exponential backoff ─────────────────────────────────────────
async function withRetry(fn, maxAttempts = 3, baseDelay = 500) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (e) {
const isNetwork = e.cause?.code === "ECONNREFUSED" || e.message.includes("fetch failed");
if (attempt === maxAttempts || !isNetwork) throw e;
const delay = baseDelay * 2 ** (attempt - 1);
console.error(` Retry ${attempt}/${maxAttempts} in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
}
}
// ── CLI ────────────────────────────────────────────────────────────────────
const apiKey = process.env.OWM_API_KEY;
if (!apiKey) {
console.error("Error: set the OWM_API_KEY environment variable.");
console.error(" Get a free key at https://openweathermap.org/api");
console.error(" Then: export OWM_API_KEY=your_key_here");
process.exit(1);
}
const city = process.argv.slice(2).join(" ");
if (!city) {
console.error("Usage: node weather.mjs <city name>");
console.error(" Example: node weather.mjs Mumbai");
process.exit(1);
}
try {
const data = await withRetry(() => fetchWeather(city, apiKey));
console.log(formatWeather(data));
} catch (e) {
console.error("Error:", e.message);
process.exit(1);
}
4. Test & Prove
import { formatWeather } from "./weather.mjs";
// Mock response matching the OpenWeatherMap API shape:
const mock = {
name: "Mumbai",
sys: { country: "IN", sunrise: 1719456600, sunset: 1719502800 },
main: { temp: 308.15, feels_like: 315.0, humidity: 88 },
weather: [{ description: "broken clouds" }],
wind: { speed: 4.5 },
clouds: { all: 75 },
};
let pass=0, fail=0;
function test(desc,fn){try{fn();console.log(" ✓",desc);pass++;}catch(e){console.log(" ✗",desc,"—",e.message);fail++;}}
const output = formatWeather(mock);
test("contains city name", () => { if (!output.includes("Mumbai")) throw new Error(); });
test("shows country code", () => { if (!output.includes("IN")) throw new Error(); });
test("converts K to C", () => { if (!output.includes("35.0°C")) throw new Error(output); });
test("shows humidity", () => { if (!output.includes("88%")) throw new Error(); });
test("converts m/s to km/h", () => { if (!output.includes("16.2")) throw new Error(output); }); // 4.5*3.6
test("shows description", () => { if (!output.includes("Broken clouds")) throw new Error(output); });
console.log(\`
\${pass} passed, \${fail} failed\`);
5. The Interface
Setup: Get a free API key at openweathermap.org/api (free tier: 60 calls/minute).
Input: city name as CLI arguments. Multi-word cities work: node weather.mjs New Delhi.
export OWM_API_KEY=your_key_here node weather.mjs Mumbai 📍 Mumbai, IN Broken clouds 🌡 35.0°C (feels like 42.1°C) 💧 Humidity: 88% 💨 Wind: 16.2 km/h ☁️ Cloud cover: 75% 🌅 Sunrise: 6:10:00 AM 🌇 Sunset: 7:00:00 PM
6. Run It
export OWM_API_KEY=your_key_here # set once per terminal session
node weather.mjs Mumbai
node weather.mjs "New Delhi"
node weather.mjs London
node weather.test.mjs # runs without API key🎯 Try this next
- Add a 5-day forecast using the
/forecastendpoint - Cache responses in a local JSON file for 10 minutes (avoid repeated API calls)
- Add a
--jsonflag to output raw JSON for piping into other tools - Build a browser version: a form that fetches and displays weather without a backend
What you practised
The real fetch + async/await + error-handling pattern used in every JS app that talks to an API.
Environment variables for secrets (process.env). Retry with exponential backoff.
Separating the pure formatting function (testable without network) from the impure fetch function.
Reference: Async & Promises · Error Handling · Date & Intl