The US National Weather Service and weather.gov is an amazing resource, and has an easy to use, well documented API. I really appreciate public resources like these and want to rework an old script of mine, so I decided to document and expand on their examples with by own.

Alrighty, let's fetch a forecast for a given latitude and longitude!

View Source Run Demo

Learning about the API

The first step, but not so flashy step is fiddling with and learning about the API.

Documentation

NWS API Web Service Documentation Open API Documentation Endpoint

Looks great! Everything we need to be dangerous.

Accessing forecast data

Here we find the implementation details need to reach our goal.

In the Examples section of the generously provided official docs, we learn that forecasts are provided by an office for a given set of grid coordinates.

https://api.weather.gov/gridpoints/{office}/{grid X},{grid Y}/forecast

The examples even go on to show how to collect that info from a given point!

https://api.weather.gov/points/{latitude},{longitude}

From looking at sample forecast, we've got an array of objects to make up our forecast. Right on.

{
  "number": 4,
  "name": "Monday",
  "startTime": "2020-10-26T06:00:00-05:00",
  "endTime": "2020-10-26T18:00:00-05:00",
  "isDaytime": true,
  "temperature": 26,
  "temperatureUnit": "F",
  "temperatureTrend": null,
  "windSpeed": "10 to 15 mph",
  "windDirection": "N",
  "icon": "https://api.weather.gov/icons/land/day/snow,70/snow,50?size=medium",
  "shortForecast": "Light Snow Likely",
  "detailedForecast": "Snow likely. Mostly cloudy, with a high near 26. North wind 10 to 15 mph, with gusts as high as 25 mph. Chance of precipitation is 70%. New snow accumulation of 1 to 3 inches possible."
}

User Agent

weather.gov wants us to use an User Agent to identify ourselves, however as of writing that isn't supported in all browsers.

Planning

Now with some information in hand, let's state what we want to do:

For a given latitude and longitude, use the NWS API to make a 5 day forecast like you might see on TV.

I'm going to use a class with ES modules to organize and execute our code, but keep it fairly functional in case I need to reuse it in the future.

With our goal, and some knowledge of the API in hand let's sketch out the code:

export class ForecastFetcher {
  // set defaults, merge user config in constructor

  // fetch data via the nws api

  // lookup and provide point 

  // lookup and provide forecast

  // combine point and forecast lookups

  // prepare forecast days and render template
}

Fetching data

We've gotta get the data from the API so let's setup a tool to query it. We're going to use a super simple fetch with the GET method.

This will definitely happen more than once, so let's abstract it.

// fetch data via the nws api
useNWS = async (route) => {
  const response = await fetch(`${this.config.host}${route}`, {
    method: "GET",
  });

  return response.json();
};

Now we can make functions to request data from our two endpoints:

// lookup and provide point information
lookupPoint = async (lat, lng) => {
  return this.useNWS(`points/${lat},${lng}`);
};

// lookup and provide forecast info
lookupForecast = async (office, gridX, gridY) => {
  return this.useNWS(`gridpoints/${office}/${gridX},${gridY}/forecast`);
};

Business Logic

Now let's take what we have, and turn it into what we need. In a highly used situation you would want to persist data from the first endpoint, but for now let's make a function to combine our class's abilities:

// combine point and forecast lookups
lookupForecastForLatLng = async (lat, lng) => {
  const point = await this.lookupPoint(lat, lng);
  const { cwa, gridX, gridY } = point.properties;

  return await this.lookupForecast(cwa, gridX, gridY);
};

Parsing the forecast

The properties key of the response json contains an array of forecasts called periods. We need to loop over these and construct a forecast. There are different ways to do this but a quick and dirty innerHTML suits my current needs.

There's a small catch, sometimes the first item is the current night! We'll need to handle that edge case.

// manipulate html strings, or let user do it
markupForecast = (forecast) => {
  let forecastMarkup = "";
  const { periods } = forecast.properties;

  let offset = 0;
  let maxDays = this.config.maxDays;

  if (!periods[0].isDaytime) {
    offset = 1;
    maxDays -= 1;
    forecastMarkup += this.dayRenderer({ night: periods[0] });
  }

  for (let i = offset; i < maxDays * 2; i += 2) {
    const forecastDay = {
      day: periods[i],
      night: periods[i + 1],
    };

    forecastMarkup += this.dayRenderer(forecastDay);
  }

  const forecastWrapper = document.createElement("DIV");
  forecastWrapper.innerHTML = this.wrapRenderer(forecast, forecastMarkup);
  return forecastWrapper;
};

Templating

Before the markup is inserted into the DOM, it much first be collected by our templating functions. We'll use template literals (``) and a single argument passed as an object to make this highly flexible.

See dayRenderer, and wrapRenderer in the source.

View Source

And... Done!

Our final function returns a element ready to DOM inseration. See the bottom of the script for our new tool at use!

// Abbreviated

(async () => {
  const demo = document.querySelector("#demo");

  // instantiate the class with our config
  const fetcher = new ForecastFetcher({
    maxDays: 5,
  });

  // Lookup forecast for lat and lng
  const forecast = await fetcher.lookupForecastForLatLng(39.7456, -97.0892);

  // mark it up and stick it in the DOM
  const forecastNode = fetcher.markupForecast(forecast);
  demo.appendChild(forecastNode);
})();
      

Thanks for reading!

Note

This all started with wanting to renovate an old hairy and scary jQuery script I'd wrote that parsed XML layouts from a different NWS API. It worked at the time but I couldn't bring that cruft forward.

I had no ideas these endpoints existed at the time, and it just goes to show there's always one more article to read. Thanks to everyone whose work made this API public and so easy to use!