npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@truewire/weather-gov

v0.1.0

Published

Typed, validated TypeScript client for the US National Weather Service API, generated by Truewire.

Readme

weather.gov, typed — TypeScript

A typed, validated TypeScript client for the US National Weather Service API, generated by Truewire from the same spec as the Python client and proven against the same recordings.

No account, no key, no quota. The service asks one thing of a caller: say who you are in User-Agent. That is the contact option, and it is required.

Install

npm install @truewire/weather-gov

Node 20 or newer, or any runtime with fetch and URLSearchParams — the transport is fetch and nothing else, so Deno, Bun and the browser work unchanged. api.weather.gov sends Access-Control-Allow-Origin: *, so a browser is a real target here rather than a theoretical one.

Use

Almost everything in this API is addressed by grid cell, not by coordinates, so a caller starts at points.getPoint and uses what it returns:

import { Weather } from '@truewire/weather-gov'

const client = Weather.new({ contact: '[email protected]' })

const point = await client.points.getPoint({ latitude: 47.6062, longitude: -122.3321 })
console.log(point.gridId, point.gridX, point.gridY, point.timeZone)

const forecast = await client.forecast.getForecast({
  office: point.gridId,
  grid_x: point.gridX,
  grid_y: point.gridY,
})
for (const period of forecast.periods.slice(0, 3)) {
  console.log(period.name, `${period.temperature}°${period.temperatureUnit}`, period.shortForecast)
}
SEW 125 68 America/Los_Angeles
Tonight 56°F Mostly Clear
Wednesday 74°F Sunny
Wednesday Night 57°F Partly Cloudy

Lifecycle: there isn't one

Weather.new(...) returns a client and that is the whole of it. There is nothing to close, no await using, no dispose: the transport is fetch and a base URL, and the client owns no connection, no pool and no socket. Build one at module scope and keep it, or build one per request; neither leaks anything.

That is a property of this API rather than of Truewire — the Bluesky client holds WebSocket connections to the firehose and does implement AsyncDisposable, because there it has something to give back.

AbortSignal is per call, for the same reason: there is no client-level cancellation when there is no client-level resource.

import { Weather } from '@truewire/weather-gov'

const client = Weather.new({ contact: '[email protected]' })

const timeout = AbortSignal.timeout(5_000)
const alerts = await client.alerts.getActiveAlerts(
  { status: ['actual'], severity: ['Severe'] },
  { signal: timeout },
)
console.log(alerts.features.length, 'severe alerts in effect')

Every measurement carries its unit

This API never sends a bare number. A temperature is { unitCode: 'wmoUnit:degC', value: 13, qualityControl: 'V' }, and value is null wherever the measurement is missing rather than zero — an airport station reports windGust only when there were gusts. value is typed number | null, so the null is impossible to forget:

import { Weather } from '@truewire/weather-gov'

const client = Weather.new({ contact: '[email protected]' })

const now = await client.stations.getLatestObservation({ station_id: 'KSEA' })
console.log(now.stationName, 'at', now.timestamp.toISOString())
console.log(' temperature', now.temperature.value, now.temperature.unitCode)

const gust = now.windGust?.value
console.log(' gusting to', gust ?? '(not gusting)')

for (const layer of now.cloudLayers) {
  console.log(' ', layer.amount, 'at', layer.base.value, 'm')
}

Note now.timestamp.toISOString(): a wire timestamp arrives as a Date, not as the string it travelled as. The spec declares the format and the codec does the conversion, in both directions — a start passed as a Date is rendered back to the ISO 8601 the service parses.

The wrapper is gone where it carried nothing

Most of this API answers in GeoJSON. For a forecast, the Feature wrapper's geometry is the outline of the grid cell you just named, and its properties is the forecast — so those endpoints declare an envelope and the client hands back the forecast. Endpoints whose geometry is real data, like an alert's polygon, are returned whole:

import { Weather } from '@truewire/weather-gov'

const client = Weather.new({ contact: '[email protected]' })

// Enveloped: `.periods`, not `.properties.periods`.
const forecast = await client.forecast.getForecast({ office: 'SEW', grid_x: 125, grid_y: 68 })
console.log(forecast.periods.length, 'periods')

// Not enveloped: the alert's polygon is data you asked for.
const alerts = await client.alerts.getActiveAlerts({ status: ['actual'], severity: ['Severe'] })
for (const feature of alerts.features.slice(0, 5)) {
  const where = feature.geometry ? feature.geometry.type : 'zones only'
  console.log(feature.properties.event, '—', feature.properties.areaDesc.slice(0, 40), `(${where})`)
}

Filters that disagree about capitalisation

status takes actual; severity, urgency and certainty take Severe, Immediate, Likely. A client that lower-cased all four uniformly would get a 400 from three of them. The enumerations here come from the service's own error replies, so the mistake is a type error rather than a round trip:

import { Weather } from '@truewire/weather-gov'

const client = Weather.new({ contact: '[email protected]' })

await client.alerts.getActiveAlerts({
  status: ['actual'],
  // @ts-expect-error -- the service spells this one `Severe`, and the type says so.
  severity: ['severe'],
})

What the types cannot catch, the error mapping can:

import { BadRequest } from '@truewire/core'
import { Weather } from '@truewire/weather-gov'

const client = Weather.new({ contact: '[email protected]' })

try {
  // A zone code is `WAZ315`-shaped, constrained by a regex rather than an enumeration --
  // so this one is only caught on the wire.
  await client.alerts.getActiveAlerts({ zone: ['NOPE'] })
} catch (error) {
  if (error instanceof BadRequest) console.log(error.message)
}
GET /alerts/active: HTTP 400: query.zone[0]: Does not match the regex pattern
^(A[KLMNRSZ]|C[AOT]|D[CE]|...)[CZ]\d{3}$

Turning validation off

Responses are validated against their declared shapes by default. A call can opt out, and the return type becomes unknown rather than a lie:

import { Weather } from '@truewire/weather-gov'

const client = Weather.new({ contact: '[email protected]' })

const raw: unknown = await client.forecast.getGridData(
  { office: 'SEW', grid_x: 125, grid_y: 68 },
  { validate: false },
)
console.log(typeof raw)

Passing validate: false to Weather.new makes that the default for the whole client, without changing any return type: a client-level default cannot make getForecast return unknown for one caller and a GridpointForecast for another.

What is covered

The same eleven endpoints as the Python client, in the same six groups, from the same spec: points, forecast, stations, alerts, offices, products. The method names are camelCase here and snake_case there; nothing else differs.

Tests

yarn test replays every recording in spec/ through this client against truewire mock, which serves them over real HTTP. Nothing in the tests touches the network. The assertions mirror packages/python/test/test_recordings.py one for one, so a divergence between the two clients is a test failure rather than a discovery months later.

Every code block on this page is compiled against the package by yarn test, so a snippet here cannot rot past the client.

Generated by Truewire

The endpoint classes, the types and the codecs are generated by truewire generate typescript and committed. The core — the transport, the User-Agent, the envelope, the error mapping — is written by hand and never touched by regeneration.

Generated code imports nothing from this package: it names the contract interfaces (HttpEndpoint<DefaultMeta>) and receives an object that satisfies them structurally. That is why Weather.new lives on a subclass in core/client.ts rather than on the generated class.

License

MIT.