Build a Weather CLI with Node.js

Build a Node.js CLI that fetches live weather for any city.

Introduction

30 Second Summary

Checking the weather often means opening a website that shows more detail than you need. Sometimes you only want the current conditions for one city.

In this project, you will build a command-line weather lookup with Claude Code and Node.js. Your app passes coordinates from the Open-Meteo Geocoding API into the Open-Meteo Weather Forecast API to retrieve live conditions.

What You'll Build

Picture entering London in Windows PowerShell and seeing its live weather appear as a clean terminal report.

By the end of this project, you'll have:

  • Live weather lookups that turn a city name into a current terminal report.
  • Multi-word city support that handles names such as New York without source code changes.
  • Readable failure messages that explain missing input, unknown cities, or failed requests.
  • Secret Mission: Add a --fahrenheit switch that displays temperature in °F with wind speed in mp/h.

Are there any prerequisites?

You'll need Windows, internet access, and working Claude Code account access. You do not need an Open-Meteo API key or an npm package.

Before We Start

The Windows command-line tool you are about to build turns a city name into live weather. This checkpoint captures why you want to learn the chained geocoding-to-weather API flow behind it.

Verify Your Windows Tooling

Your weather CLI needs a dependable Node.js runtime before it can reach live APIs. A mismatched runtime can make an API problem look like a JavaScript problem.

An unauthenticated Claude Code session creates a second source of confusion. You'll verify each tool in Windows PowerShell before any code exists.

In this step, get ready to:
  • Verify Node.js v24.20.0 LTS and Claude Code.
  • Create an empty weather-cli project folder.
  • Authenticate Claude Code inside the project folder.
Verify Node.js and Claude Code

Version checks give you a clean baseline for the project. They confirm that PowerShell can find both tools before you depend on them.

  • Check your installed Node.js version by running this command in Windows PowerShell:
node --version

What does this check prove?

Node.js prints the version of the runtime that PowerShell can currently access. This project uses v24.20.0 LTS.

✔️ I see version v24.20.0

That's your runtime sorted. The v24.20.0 output confirms that Node.js is ready for this project.

ⓧ I see a different version

Your installed version does not match the runtime used in this project. Install the current LTS release before continuing.

  • Visit the official Node.js download page.
  • Install the current LTS release by following the Windows instructions on the page.
  • Return to Windows PowerShell after the installation finishes.
  • Repeat the Node.js version check shown above.

The version check should now print v24.20.0.

ⓧ Command not found

PowerShell cannot access Node.js yet. Install the current LTS release to add the runtime to Windows.

  • Visit the official Node.js download page.
  • Install the current LTS release by following the Windows instructions on the page.
  • Restart Windows PowerShell after the installation finishes.
  • Repeat the Node.js version check shown above.

The version check should now print v24.20.0.

Still seeing the wrong Node.js version?

  • Restart Windows PowerShell so it reloads the commands available on your computer.
  • Run the version check again after confirming that the current LTS installation completed.
  • Still stuck? Help me verify Node.js v24.20.0 in Windows PowerShell.
  • Check your installed Claude Code version by running this command in Windows PowerShell:
claude --version

What does this check prove?

Claude Code prints its installed version followed by (Claude Code). That ending confirms PowerShell has found the correct command-line tool.

✔️ I see a Claude Code version

Claude Code is installed correctly. The output should end with (Claude Code).

ⓧ Command not found

PowerShell cannot access Claude Code yet. The official installer adds the command-line tool to Windows.

  • Install Claude Code by running this command in Windows PowerShell:
irm https://claude.ai/install.ps1 | iex

What does this command do?

This downloads the official Claude Code installation script. PowerShell runs the script to install the command-line tool.

  • Restart Windows PowerShell after the installer finishes.
  • Repeat the Claude Code version check shown above.

You should now see a version number ending in (Claude Code).

Claude Code still unavailable?

  • Restart Windows PowerShell so it can detect the newly installed command.
  • Repeat the installer if the first attempt stopped before completing.
  • Still stuck? Help me install Claude Code in Windows PowerShell.
Give Claude Code a clean workspace

A dedicated project folder keeps the weather CLI separate from unrelated files. Starting with an empty folder also makes every file Claude Code creates easy to identify.

  • Create the weather-cli folder from the Windows PowerShell window you used above by running these commands:
New-Item -ItemType "Directory" -Path ".\weather-cli"
Set-Location -Path ".\weather-cli"

What do these commands do?

  • The New-Item command creates the empty weather-cli directory.
  • The Set-Location command moves PowerShell into that directory. Commands you run next use this folder as their workspace.
  • Confirm your current folder by reading the path in the PowerShell prompt.

You'll see that the path ends with weather-cli.

Project folder not opening?

  • If the folder already exists, run only the Set-Location line from the command block above.
  • Check that the folder name is exactly weather-cli if the path does not change.
  • Still stuck? Help me create and enter the weather-cli folder in Windows PowerShell.
Authenticate Claude Code in the project folder

Claude Code starts an interactive session inside your current folder. On first use, it may send you to a browser to authenticate your account.

Before you run it, which folder do you expect Claude Code to show as its working directory?

  • Start Claude Code from the weather-cli folder by running this command:
claude

What happens when Claude Code starts?

The claude command opens an interactive coding session in the current directory. Authentication connects that session to your existing Claude access.

  • Complete browser authentication if Claude Code prompts you.
  • Return to the interactive terminal session after authentication finishes.
  • Confirm that the Claude Code prompt shows weather-cli as its working directory.

You'll see the Claude Code prompt with weather-cli as its working directory.

The earlier v24.20.0 output proves Node.js is ready. The version text ending in (Claude Code) proves Claude Code is installed.

That clears the setup uncertainty. Your authenticated assistant is now anchored to the right project folder.

Claude Code not showing the project folder?

  • Return to the Windows PowerShell window from the folder setup.
  • Run the Set-Location line from the previous substep again.
  • Start Claude Code again after the PowerShell path ends with weather-cli.
  • Still stuck? Help me authenticate Claude Code in the weather-cli folder.

Your Windows toolchain is ready. Next, you'll make your first real geocoding request and inspect the location data it returns.

Make Your First API Request

Your Node.js runtime is ready inside the empty weather-cli folder. Your authenticated Claude Code session can now turn that empty folder into a working command-line app.

A REST API request lets your script ask another service for structured data. This step sends a city to a location service. The returned JSON reveals what the service knows about that place.

In this step, get ready to:
  • Use Claude Code to draft a geocoding request for London.
  • Understand how the request becomes parsed JSON.
  • Run the script to inspect the live location response.
Draft the geocoding request

The Open-Meteo Geocoding API converts a place name into location data. Its query parameters narrow the search to one English JSON result.

  • Switch back to the authenticated Claude Code session in the weather-cli folder from earlier.
  • Ask Claude Code to propose the first version of weather.mjs by entering this prompt:
Draft a new weather.mjs file for Node.js 24.20.0.
Do not create the file yet.

const city = 'London';

const query = new URLSearchParams({
  name: city,
  count: '1',
  language: 'en',
  format: 'json',
});

const response = await fetch(
  `https://geocoding-api.open-meteo.com/v1/search?${query}`,
);

if (!response.ok) {
  throw new Error('The API request failed.');
}

const data = await response.json();
console.log(data);

After the draft, explain the complete request URL, await, response.ok, and response.json() in beginner-friendly language.
Wait for my approval before creating the file.

What does this request do?

  • The city value keeps the first request focused on London.
  • URLSearchParams serializes the search values into a query string.
  • fetch() sends the request to the geocoding endpoint.
  • response.ok protects the parsing step from an unsuccessful HTTP response.
  • response.json() parses the response body into the data value.
Review the request before creation

Claude Code's explanation gives you a chance to check the logic before the file changes. Each part should connect the request to the final terminal output.

  • Check that Claude Code explains how query becomes part of the complete endpoint URL.
  • Check that Claude Code explains why await pauses each asynchronous operation.
  • Check that Claude Code explains how response.ok protects the JSON parsing step.
  • Create weather.mjs by entering this approval prompt:
Create weather.mjs with the exact draft above.
After saving it, show me the full file so I can compare it with the approved version.

What happens when you approve this?

Claude Code writes the approved draft to weather.mjs. Showing the saved file gives you an immediate comparison point.

  • Compare the displayed file with the approved draft.
  • Confirm the first line is const city = 'London';.
  • Confirm the final line is console.log(data);.

Does the saved file differ?

  • Use the original draft in your prompt as the exact comparison source.
  • Check that the saved filename is weather.mjs.
  • Check that Claude Code is still working inside the weather-cli folder.
  • Still stuck? Help me compare my weather.mjs file with the approved geocoding draft.
Run the live request

The saved file now contains one external request. Running it proves whether Node.js can reach Open-Meteo from your project.

Before you run it, which fields do you expect the geocoding service to return for London?

  • Ask Claude Code to run the script from the current weather-cli folder by entering this prompt:
Run node weather.mjs in the current weather-cli folder.
Show me the complete terminal output.
Help me inspect the first object in the results array.

What does this run prove?

  • Node.js loads weather.mjs as an ECMAScript module.
  • The module format allows the request to use top-level await.
  • The built-in fetch() function reaches the external endpoint without an npm package.
  • console.log(data) prints the parsed response for inspection.

You'll see a results array containing London. Its first location object includes latitude plus longitude.

The output contains no temperature field. That intended gap shows the first request only solved the location part of the weather lookup.

Didn't get the London response?

  • Check that Claude Code's displayed working directory ends in weather-cli if the script file is missing.
  • Confirm that weather.mjs matches the approved draft if Node.js reports a syntax problem.
  • Confirm your internet connection if the request cannot reach Open-Meteo.
  • Still stuck? Help me diagnose why my first Open-Meteo request is not returning London location data.

✔️ Awesome, I've got everything!

That worked. Your script now reaches Open-Meteo. It also parses the live JSON response.

ⓧ I'd like to double check the full code

  • Compare your saved weather.mjs with this reference:
const city = 'London';

const query = new URLSearchParams({
  name: city,
  count: '1',
  language: 'en',
  format: 'json',
});

const response = await fetch(
  `https://geocoding-api.open-meteo.com/v1/search?${query}`,
);

if (!response.ok) {
  throw new Error('The API request failed.');
}

const data = await response.json();
console.log(data);

What should match?

  • The file begins with the hard-coded London value.
  • The query includes four geocoding parameters.
  • The response check appears before JSON parsing.
  • The final line prints the parsed data value.

Your first live request is working. Next, you'll turn the returned coordinates into a readable current-weather report.

Fetch Current Weather

Your first request proved that Node.js can reach the Open-Meteo Geocoding API. The raw JSON identifies London with coordinates.

The Open-Meteo Forecast API turns those coordinates into current conditions. Claude Code will help you chain both requests into one formatted terminal report.

In this step, get ready to:
  • Refactor the existing request into reusable functions.
  • Pass London's coordinates into the Forecast API.
  • Run a formatted current-weather report.
Refactor the shared request logic

Both API calls need the same request handling. A shared fetchJson() function keeps that work in one place.

What should the refactor contain?

  • The city value remains set to London.
  • The fetchJson(url) function makes the request. It checks response.ok before returning response.json().
  • The getLocation(cityName) function builds the geocoding query. It returns data.results[0].
  • Switch back to the Claude Code prompt in the authenticated weather-cli folder.
  • Ask Claude Code to refactor weather.mjs to match the plan above.
  • Review the proposed changes in weather.mjs.
  • Accept the edit after both functions match the plan.

Did Claude Code change too much?

  • Keep const city = 'London'; at the top of weather.mjs.
  • Check that the function names match fetchJson and getLocation exactly.
  • Ask Claude Code to revise only the existing request logic if unrelated code changes appear.
  • Still stuck? Help me review Claude Code's refactor of my geocoding request.
Chain coordinates into the Forecast API

The location response provides the latitude and longitude required by the weather request. The second response contains the current conditions and their units.

How does the request chain work?

  • The getCurrentWeather(latitude, longitude) function receives the coordinates from the location result.
  • Its URLSearchParams query requests temperature_2m, apparent_temperature, and wind_speed_10m.
  • The timezone=auto parameter requests local observation time for the coordinates.
  • The main() function awaits the location. It passes the returned coordinates into getCurrentWeather().
  • The line const { current, current_units: units } = weather; gives the report short names for the weather values and their units.
  • Ask Claude Code to extend weather.mjs to match the request chain above.
  • Review the proposed getCurrentWeather() function.
  • Review the proposed main() function.
  • Accept the edit after the report lines use current and units.

✔️ Awesome, I've got everything!

  • Confirm Claude Code saved weather.mjs.

ⓧ I'd like to double check the full code

const city = 'London';

async function fetchJson(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error('The API request failed.');
  }

  return response.json();
}

async function getLocation(cityName) {
  const query = new URLSearchParams({
    name: cityName,
    count: '1',
    language: 'en',
    format: 'json',
  });

  const data = await fetchJson(
    `https://geocoding-api.open-meteo.com/v1/search?${query}`,
  );

  return data.results[0];
}

async function getCurrentWeather(latitude, longitude) {
  const query = new URLSearchParams({
    latitude: String(latitude),
    longitude: String(longitude),
    current: 'temperature_2m,apparent_temperature,wind_speed_10m',
    timezone: 'auto',
  });

  return fetchJson(`https://api.open-meteo.com/v1/forecast?${query}`);
}

async function main() {
  const location = await getLocation(city);
  const weather = await getCurrentWeather(location.latitude, location.longitude);
  const { current, current_units: units } = weather;

  console.log(`\nCurrent weather for ${location.name}, ${location.country}`);
  console.log(`Observed: ${current.time} (${weather.timezone})`);
  console.log(`Temperature: ${current.temperature_2m}${units.temperature_2m}`);
  console.log(`Feels like: ${current.apparent_temperature}${units.apparent_temperature}`);
  console.log(`Wind: ${current.wind_speed_10m} ${units.wind_speed_10m}`);
}

await main();

How does the finished file work?

  • The fetchJson() function handles both HTTP requests.
  • The getLocation() function converts London into coordinates.
  • The getCurrentWeather() function converts those coordinates into current conditions.
  • The main() function connects the results and prints the report.
Run the formatted weather report

The formatted report proves that the two requests work as one flow. Live values may change between runs because the second request retrieves current conditions.

Before you run it, which weather details do you expect the second request to add to the location data?

  • Ask Claude Code to run your finished script with this command:
node weather.mjs

What should I see?

Claude Code runs the script with Node.js. You'll see a formatted report instead of raw location JSON.

  • The first line names the matched location.
  • The Observed line shows the local observation time.
  • The Temperature line shows the current temperature.
  • The Feels like line shows the apparent temperature.
  • The Wind line shows the current wind speed.

Still seeing raw JSON?

  • Ask Claude Code to remove the old console.log(data); line if raw geocoding JSON still appears.
  • Compare each function name with the full-code tab if the terminal reports an undefined function.
  • Confirm your Windows computer still has internet access if no weather response arrives.
  • Still stuck? Help me debug my chained weather API requests.

You now have a CLI that turns London's coordinates into a readable live-weather report.

London is still hard-coded. Next, you'll let the person running the CLI choose any city.

Accept City Arguments

Your script already turns London's coordinates into a formatted weather report. Every run still asks for London because the city lives inside weather.mjs.

In this step, Node.js reads command-line arguments so each run can choose a city. An invented city then tests the assumption that every lookup returns a match.

In this step, get ready to:
  • Replace the hard-coded city with command-line input.
  • Verify single-word and multi-word city arguments.
  • Probe how the script responds when no location matches.
Read the city from the command line

Node.js places the values from a script command inside process.argv. Your script can turn those values into the city name for each run.

  • Return to weather.mjs in the Claude Code session from earlier.
  • Find the hard-coded city line shown below:
const city = 'London';

What does the current line do?

This constant fixes every lookup to London. Terminal input cannot change its value.

  • Replace the hard-coded line with the command-line expression shown below:
const city = process.argv.slice(2).join(' ').trim();

How does the new line work?

  • process.argv holds the executable path followed by the script path followed by the supplied arguments.
  • slice(2) keeps the supplied arguments.
  • join(' ') combines separate city words with spaces.
  • trim() removes stray space around the finished city name.
  • Save weather.mjs.
Test valid city arguments

A command-line interface takes input when you run it. Two city shapes confirm that the source file no longer controls the lookup.

  • Ask Claude Code to run both valid-city checks with these commands:
node weather.mjs Paris
node weather.mjs "New York"

What do these runs prove?

  • The first run passes Paris into the existing weather flow.
  • The second run passes the multi-word value New York into the same flow.
  • Both commands use the same weather.mjs file without a source-code edit.

You should see one current-weather report for Paris. The second report should name New York.

Still seeing London?

  • Confirm that you saved weather.mjs after replacing the city line.
  • Check that the first line contains process.argv.slice(2).join(' ').trim().
  • Confirm that Claude Code is still working inside the weather-cli folder.
  • Still stuck? Help me debug why weather.mjs keeps showing London after I pass another city.
Probe an unknown city

Valid cities cover the successful path. An invented city tests the assumption that every lookup produces a location object.

Before you run this, predict whether the lookup can produce a location object for an invented city.

  • Probe the lookup with an unknown city by running this command:
node weather.mjs ThisCityShouldNotExist987654

This crash is intentional

The run ends with a JavaScript runtime failure before it can print weather. This is the intended shortfall for this test.

The Open-Meteo Geocoding API returned no results array. getLocation() accesses data.results[0] without checking for that missing value.

  • Return to the Claude Code prompt from earlier.
  • Ask Claude Code to inspect the failed run without changing weather.mjs.
  • Ask Claude Code to identify which property is absent from the API response.

Claude Code should trace the failure to the missing results array in the location response.

✔️ Awesome, I've got everything!

Your weather.mjs file now reads valid city names from command-line arguments.

ⓧ I'd like to double check the full code

const city = process.argv.slice(2).join(' ').trim();

async function fetchJson(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error('The API request failed.');
  }

  return response.json();
}

async function getLocation(cityName) {
  const query = new URLSearchParams({
    name: cityName,
    count: '1',
    language: 'en',
    format: 'json',
  });

  const data = await fetchJson(
    `https://geocoding-api.open-meteo.com/v1/search?${query}`,
  );

  return data.results[0];
}

async function getCurrentWeather(latitude, longitude) {
  const query = new URLSearchParams({
    latitude: String(latitude),
    longitude: String(longitude),
    current: 'temperature_2m,apparent_temperature,wind_speed_10m',
    timezone: 'auto',
  });

  return fetchJson(`https://api.open-meteo.com/v1/forecast?${query}`);
}

async function main() {
  const location = await getLocation(city);
  const weather = await getCurrentWeather(location.latitude, location.longitude);
  const { current, current_units: units } = weather;

  console.log(`\nCurrent weather for ${location.name}, ${location.country}`);
  console.log(`Observed: ${current.time} (${weather.timezone})`);
  console.log(`Temperature: ${current.temperature_2m}${units.temperature_2m}`);
  console.log(`Feels like: ${current.apparent_temperature}${units.apparent_temperature}`);
  console.log(`Wind: ${current.wind_speed_10m} ${units.wind_speed_10m}`);
}

await main();

Your script now accepts city names from the terminal. Next, you'll turn the exposed missing-result crash into a readable response.

Handle Errors Gracefully

Your CLI now accepts single-word cities plus multi-word cities. An unknown city still leaves location undefined.

That missing value triggers a JavaScript runtime failure when main() reads its coordinates.

This step turns each predictable failure into a readable terminal message. It also preserves the successful weather report.

In this step, get ready to:
  • Guard missing city input.
  • Validate the first geocoding result.
  • Confirm clear output on each path.
Guard missing city input

Command-line arguments can be empty when someone runs the script without a city. A guard can stop that empty request before it reaches the API.

  • In weather.mjs, find the main() function.
  • Select the function through its closing brace above await main();.
  • Replace the selection with this version:
async function main() {
  if (!city) {
    console.error('Usage: node weather.mjs <city>');
    process.exitCode = 1;
    return;
  }

  try {
    const location = await getLocation(city);
    const weather = await getCurrentWeather(
      location.latitude,
      location.longitude,
    );
    const { current, current_units: units } = weather;

    console.log(`\nCurrent weather for ${location.name}, ${location.country}`);
    console.log(`Observed: ${current.time} (${weather.timezone})`);
    console.log(
      `Temperature: ${current.temperature_2m}${units.temperature_2m}`,
    );
    console.log(
      `Feels like: ${current.apparent_temperature}${units.apparent_temperature}`,
    );
    console.log(`Wind: ${current.wind_speed_10m} ${units.wind_speed_10m}`);
    console.log('Source: Open-Meteo.com');
  } catch (error) {
    console.error(`Could not get weather: ${error.message}`);
    process.exitCode = 1;
  }
}

What does this function change?

  • The if (!city) guard prints the usage message before any request starts.
  • The try...catch boundary converts thrown errors into one readable terminal message.
  • The existing response.ok check already turns failed HTTP responses into errors that this boundary can catch.
  • Setting process.exitCode = 1 tells Node.js that the command failed while allowing output to finish normally.
  • The final log line gives Open-Meteo attribution in every successful report.
  • Save weather.mjs.

Before you test it, predict what the CLI should do when its required city is missing:

  • Test the empty-input path by running this command:
node weather.mjs

What does this check prove?

The command supplies no city. The new guard handles that condition before the geocoding request begins.

You should see Usage: node weather.mjs <city> instead of a runtime failure.

That is the first failure path contained. Your CLI now explains how to use it.

Still seeing the old failure?

  • Confirm that the if (!city) block is the first code inside main().
  • Confirm that return; remains inside the missing-city guard.
  • Save weather.mjs before running the command again.
  • Still stuck? Help me debug the missing-city guard in my weather CLI.
Validate the geocoding result

The catch boundary controls how errors appear. The location lookup still needs to identify the real problem when the API returns no match.

Optional chaining safely checks the first result when the results array is missing. An explicit error can then name the city that failed.

  • In getLocation(), find return data.results[0];.
  • Replace that line with this validation block:
  const location = data.results?.[0];

  if (!location) {
    throw new Error(`No location found for "${cityName}".`);
  }

  return location;

What does this validation do?

  • The data.results?.[0] expression safely produces an undefined value when no results array exists.
  • The if (!location) check turns that missing value into a specific error.
  • Returning location preserves the working path for valid cities.
  • Save weather.mjs.

Before you run the unknown city, predict whether the terminal will show a stack trace or your custom message:

  • Test the no-match path by running this command:
node weather.mjs ThisCityShouldNotExist987654

What does this request test?

This city has no matching geocoding result. The request reaches your new location check before the script tries to read coordinates.

You should see Could not get weather: No location found for "ThisCityShouldNotExist987654"..

You have replaced an internal runtime failure with guidance that names the bad input. That makes the problem clear to anyone using the CLI.

Still seeing an undefined location error?

  • Confirm that data.results?.[0] includes the question mark before the full stop.
  • Confirm that the error uses cityName inside getLocation().
  • Confirm that return location; comes after the missing-location check.
  • Still stuck? Help me debug the unknown-city validation in getLocation().

✔️ Awesome, I've got everything!

Your input guard plus location validation are now saved in weather.mjs.

ⓧ I'd like to double check the full code

const city = process.argv.slice(2).join(' ').trim();

async function fetchJson(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error('The API request failed.');
  }

  return response.json();
}

async function getLocation(cityName) {
  const query = new URLSearchParams({
    name: cityName,
    count: '1',
    language: 'en',
    format: 'json',
  });

  const data = await fetchJson(
    `https://geocoding-api.open-meteo.com/v1/search?${query}`,
  );
  const location = data.results?.[0];

  if (!location) {
    throw new Error(`No location found for "${cityName}".`);
  }

  return location;
}

async function getCurrentWeather(latitude, longitude) {
  const query = new URLSearchParams({
    latitude: String(latitude),
    longitude: String(longitude),
    current: 'temperature_2m,apparent_temperature,wind_speed_10m',
    timezone: 'auto',
  });

  return fetchJson(`https://api.open-meteo.com/v1/forecast?${query}`);
}

async function main() {
  if (!city) {
    console.error('Usage: node weather.mjs <city>');
    process.exitCode = 1;
    return;
  }

  try {
    const location = await getLocation(city);
    const weather = await getCurrentWeather(
      location.latitude,
      location.longitude,
    );
    const { current, current_units: units } = weather;

    console.log(`\nCurrent weather for ${location.name}, ${location.country}`);
    console.log(`Observed: ${current.time} (${weather.timezone})`);
    console.log(
      `Temperature: ${current.temperature_2m}${units.temperature_2m}`,
    );
    console.log(
      `Feels like: ${current.apparent_temperature}${units.apparent_temperature}`,
    );
    console.log(`Wind: ${current.wind_speed_10m} ${units.wind_speed_10m}`);
    console.log('Source: Open-Meteo.com');
  } catch (error) {
    console.error(`Could not get weather: ${error.message}`);
    process.exitCode = 1;
  }
}

await main();

What should match?

The full file keeps the request helpers from earlier. It adds the missing-input guard plus location validation around the working weather flow.

Verify failure and success paths

Reliable behavior means checking one failure path plus one success path back to back. This proves the new safeguards do not disrupt valid weather requests.

Before you run both checks, predict which command should finish with a failure code:

  • Run both end-to-end checks from the Claude Code session by using these commands:
node weather.mjs ThisCityShouldNotExist987654
node weather.mjs London

What do these checks cover?

  • The first command confirms that an unknown city produces your specific no-location message.
  • The second command confirms that a valid city still reaches both APIs.

For the first command, you should see Could not get weather: No location found for "ThisCityShouldNotExist987654"..

For the second command, you should see London's local observation time plus temperature details. The report should end with Source: Open-Meteo.com.

That is the reliability loop complete. Your CLI now handles bad input with the same care as live weather.

Secret mission

Add a Fahrenheit Switch

Your CLI currently follows Open-Meteo's default units. Add an optional --fahrenheit switch that returns temperatures in °F and wind speed in mp/h.

Clean Up Your Resources

Clean Up Your Resources

Your only teardownable project resource is the local weather-cli folder. Stopping the script leaves no running cloud resource or added project cost.

Resources you used:

  • Local weather-cli folder containing weather.mjs.

Keep everything running

No action is needed. Choose this if you want to run more city lookups or continue refining the CLI.

  • Leave the weather-cli folder on your computer.
  • Return to the authenticated weather-cli folder when you want to continue.
  • Run claude from that folder to resume your Claude Code session.

Pause - I'll come back to this later

Pausing ends your current terminal session. Your local project remains ready for another day.

  • Close the current Windows PowerShell window to end the Claude Code session.
  • Leave the weather-cli folder on your computer.
  • Return to the weather-cli folder when you are ready to continue.
  • Run claude from that folder to resume your session.

Delete - I don't want to use this again

Deleting permanently removes your local weather-cli folder. Your installed development tools stay in place.

  • Close the current Windows PowerShell window to release the project folder.
  • Use the file browser on your computer to locate the weather-cli folder.
  • Select the weather-cli folder.
  • Press the Delete key.
  • Open the deleted-items folder from your Windows desktop.
  • Choose the permanent deletion option for the selected weather-cli folder.
  • Search your computer for weather-cli in the file browser.

You should see no matching project folder. Your computer is now clear of the project's local files.

Nice Work!

Nice Work!

Nice work! You built a Node.js weather CLI with Claude Code that turns a city name into a readable live report.

You've learned how to:

  • Use flexible city input to look up single-word or multi-word places from the terminal. Run the same weather.mjs file with a new city instead of changing its source code.
  • Chain two REST API requests so Open-Meteo geocoding coordinates feed the current-weather lookup. Parse each JSON response through the built-in fetch flow.
  • Protect the CLI with readable error handling. Missing input produces a usage message. Unknown cities produce a clear location error. Failed HTTP requests produce a readable failure message.
  • Complete the optional Secret Mission by adding a --fahrenheit switch. The weather report now supports °F temperatures with wind speed in mp/h.

Ready to quiz yourself?