The Codex / Projects / Weather CLI / JavaScript
Tier 2 — Intermediate

Weather CLI

Fetch live weather from OpenWeatherMap API — async/await, fetch, env vars, retry.

🐍 Python 🟨 JavaScript

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

weather.mjs
// ── 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

weather.test.mjs
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

  1. Add a 5-day forecast using the /forecast endpoint
  2. Cache responses in a local JSON file for 10 minutes (avoid repeated API calls)
  3. Add a --json flag to output raw JSON for piping into other tools
  4. 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

Try It Yourself