Skip to content

Repository files navigation

Weather API

A TypeScript-based Vercel serverless API that fetches weather data from OpenWeatherMap's 3.0 One Call API and aviation METAR data from Garmin.

Architecture

This project uses a clean, modular architecture:

  • Base Class Pattern: All endpoints extend ApiEndpoint which handles authentication, validation, and error handling
  • Service Layer: External API calls are encapsulated in service classes (OpenWeatherMapService, GarminService)
  • Type Safety: Full TypeScript coverage with organized models in models/ directory
  • Separation of Concerns: Each endpoint focuses purely on data transformation and formatting

Project Structure

api/
  weather.ts          # OpenWeatherMap current weather by coordinates (auth required)
  zip.ts              # OpenWeatherMap current weather by zip code (auth required)
  local.ts            # NWS current conditions + hourly forecast for a fixed set of cities (public)
  metar.ts            # Garmin aviation METAR data (public)
  runway-wind.ts      # Runway/wind favorability, as JSON, SVG, or PNG diagram (public)
  nws-current.ts      # NWS current conditions (auth required)
  nws-forecast.ts     # NWS 12-hour hourly forecast (auth required)
  tools.ts            # Anthropic-compatible tool definitions (public)
  health.ts           # Health check (public)
lib/
  ApiEndpoint.ts      # Base class for all endpoints
  middleware.ts       # Validation utilities
  weatherFormatters.ts # Shared OWM response formatting
  nwsFormatters.ts    # Shared NWS response formatting
  runwayFormatters.ts # Runway crosswind/headwind/favorability calculation
  airportDiagramRenderer.ts # Airport diagram SVG builder + PNG rendering
services/
  OpenWeatherMapService.ts  # OWM API client
  GarminService.ts          # Garmin aviation data client
  NWSService.ts             # National Weather Service API client
  AopaService.ts            # AOPA runway layout data client
models/
  common/             # Shared models (ValidationError)
  weather/            # Weather-specific models
  metar/              # METAR-specific models
  nws/                # NWS-specific models
  tools/              # Tool definition models
  localWeather/       # Output shape for /api/local, composed from the NWS models
  runway/             # Runway/wind-specific models
assets/
  fonts/              # Font bundled for PNG rendering (serverless runtimes have no system fonts)

Adding New Endpoints

To add a new endpoint:

  1. Create a service class in services/ for external API communication

  2. Define your models in models/[category]/

  3. Create an endpoint class that extends ApiEndpoint:

    class MyEndpoint extends ApiEndpoint {
      protected getRequiredParams(): string[] {
        return ['param1', 'param2'];
      }
    
      protected async process(req: VercelRequest): Promise<MyOutput> {
        // Your logic here
      }
    }

Cockpit UI

The root page (/) serves a steam-gauge cockpit instrument panel that displays live METAR aviation weather data. It auto-fetches data on load and remembers the last used airport in localStorage.

Features:

  • Six SVG circle gauges: Flight Category, Temperature, Wind, Altimeter, Visibility, Dewpoint
  • Flight Category gauge uses color-coded rings: VFR (green), MVFR (blue), IFR (red), LIFR (magenta)
  • Wind gauge shows a compass rose with a directional arc tick on the bezel and speed/direction in the center
  • Sky conditions info strip and full raw METAR string displayed below the gauges
  • No authentication required — calls the public /api/metar endpoint directly

The cockpit HTML is in public/index.html, all styles live in public/cockpit.css, and gauge rendering lives in public/cockpit.js (DOM/SVG wiring) plus public/gaugeFormatting.js (pure color, formatting, and geometry helpers, unit tested).

Setup

  1. Copy .env.example to .env.local:

    cp .env.example .env.local
  2. Add your OpenWeatherMap API key and API token to .env.local:

    OPENWEATHERMAP_API_KEY=your_actual_api_key
    API_TOKEN=your_secure_random_token
    
  3. Get an API key from OpenWeatherMap (requires One Call API 3.0 subscription)

Development

Install dependencies:

npm install

Install Vercel CLI if you haven't already:

npm i -g vercel

Run the development server:

npm start

The TypeScript files will be automatically compiled by Vercel during development and deployment.

Testing & Quality

This project uses a comprehensive testing and linting infrastructure to ensure code quality:

Running Tests

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage report
npm run test:coverage

# Run tests with UI (browser-based)
npm run test:ui

Test Coverage: The project maintains 80%+ test coverage for lines/functions and 75%+ for branches, verified on every build.

Code Quality Checks

# Run all quality checks (formatting, linting, type-checking, tests)
npm run check

# Format code with Prettier
npm run format

# Check code formatting (doesn't modify files)
npm run format:check

# Lint code with ESLint
npm run lint

# Auto-fix linting issues
npm run lint:fix

# Type check with TypeScript
npm run type-check

Pre-commit Hooks

The project uses Husky and lint-staged to automatically run quality checks before each commit:

  • Formats staged files with Prettier
  • Lints staged files with ESLint (auto-fixes when possible)
  • Type-checks the entire project

This ensures all committed code meets quality standards.

Quality Assurance

All code quality checks are enforced via pre-commit hooks using Husky:

  • Before each commit: Formats, lints, and type-checks code automatically
  • 125+ tests with 97%+ coverage ensure reliability
  • Strict TypeScript mode catches errors at compile time

This ensures only high-quality, tested code makes it into the repository and gets deployed.

Testing Stack

  • Vitest: Fast, modern test runner with native TypeScript support
  • ESLint: Code linting with TypeScript-specific rules
  • Prettier: Opinionated code formatting
  • TypeScript: Strict type checking
  • Husky + lint-staged: Git hooks for pre-commit checks

API Usage

Endpoints are listed alphabetically by path below. At a glance:

Endpoint Auth required?
/api/local No
/api/metar No
/api/nws-current Yes — x-api-token header
/api/nws-forecast Yes — x-api-token header
/api/runway-wind No
/api/tools No
/api/weather Yes — x-api-token header
/api/zip Yes — x-api-token header

Local Weather Endpoint

Endpoint: /api/local

Method: GET

Authentication: None required — this endpoint is publicly accessible without an x-api-token header.

A convenience wrapper around the NWS Current Conditions and NWS Hourly Forecast endpoints below for a fixed set of cities, so callers don't need to know each city's coordinates.

Query Parameters:

  • city (optional): Which city to fetch. Default: indianapolis
    • indianapolis
    • minneapolis
    • san_antonio
    • milwaukee

Example Request:

curl "http://localhost:3000/api/local?city=minneapolis"

Response:

{
  "current": {
    "start_time": "2026-02-27T12:00:00-05:00",
    "start_time_formatted_time": "12:00 PM",
    "start_time_formatted_datetime": "02/27/2026 12:00 PM",
    "is_daytime": true,
    "temperature": 45,
    "temperature_unit": "F",
    "wind_speed": "10 mph",
    "wind_direction": "NW",
    "short_forecast": "Mostly cloudy",
    "probability_of_precipitation": 20,
    "relative_humidity": 65
  },
  "hourly": [
    {
      "start_time": "2026-02-27T13:00:00-05:00",
      "start_time_formatted_time": "01:00 PM",
      "start_time_formatted_datetime": "02/27/2026 01:00 PM",
      "is_daytime": true,
      "temperature": 47,
      "temperature_unit": "F",
      "wind_speed": "12 mph",
      "wind_direction": "W",
      "short_forecast": "Partly cloudy",
      "probability_of_precipitation": null,
      "relative_humidity": 60
    }
  ]
}
  • current (object): The current hourly period (same shape as /api/nws-current)
  • hourly (array): Next 5 hourly forecast periods after current (same shape as /api/nws-forecast, just fewer periods)
  • An unrecognized city returns a 500 with a message listing the supported cities

METAR Endpoint

Endpoint: /api/metar

Method: GET

Authentication: None required — this endpoint is publicly accessible without an x-api-token header.

Query Parameters:

  • id (optional): Airport ICAO identifier (e.g., KUMP, KJFK). Default: KUMP

Example Request:

curl "http://localhost:3000/api/metar?id=KJFK"

Response:

{
  "id": "KJFK",
  "raw_text": "KJFK 011551Z 27012KT 10SM FEW050 SCT080 18/07 A2994 RMK AO2 SLP142",
  "observation_time": "15:51 L",
  "temperature": 18,
  "dewpoint": 7,
  "wind": {
    "direction": 270,
    "speed": 12
  },
  "visibility": 10,
  "altimeter": 29.94,
  "flight_category": "VFR",
  "sky_conditions": [
    { "coverage": "FEW", "base_feet": 5000, "description": "Few at 5000ft" },
    { "coverage": "SCT", "base_feet": 8000, "description": "Scattered at 8000ft" }
  ]
}
  • id (string): Airport ICAO identifier
  • raw_text (string): Full raw METAR string
  • observation_time (string): Local observation time (e.g., "15:51 L")
  • temperature (number): Temperature in °C
  • dewpoint (number): Dewpoint temperature in °C
  • wind.direction (number): Wind direction in degrees (0–360)
  • wind.speed (number): Wind speed in knots
  • visibility (number): Visibility in statute miles (SM)
  • altimeter (number): Altimeter setting in inHg
  • flight_category (string): "VFR", "MVFR", "IFR", or "LIFR"
  • sky_conditions (array): Cloud layers, each with coverage, base_feet, and description

NWS Current Conditions Endpoint

Endpoint: /api/nws-current

Method: GET

Authentication: Required — pass a valid x-api-token header (see below).

Headers:

  • x-api-token (required): API token for authentication

Query Parameters:

  • lat (required): Latitude coordinate (US locations only)
  • lon (required): Longitude coordinate (US locations only)

Example Request:

curl -H "x-api-token: your_token_here" \
  "http://localhost:3000/api/nws-current?lat=39.7684&lon=-86.1581"

Response:

{
  "start_time": "2026-02-27T12:00:00-05:00",
  "start_time_formatted_time": "12:00",
  "start_time_formatted_datetime": "02/27/2026 12:00 PM",
  "is_daytime": true,
  "temperature": 45,
  "temperature_unit": "F",
  "wind_speed": "10 mph",
  "wind_direction": "NW",
  "short_forecast": "Mostly cloudy",
  "probability_of_precipitation": 20,
  "relative_humidity": 65
}
  • start_time (string): Full ISO 8601 timestamp of the period start (e.g., "2026-02-27T12:00:00-05:00")
  • start_time_formatted_time (string): Local time of the period in hh:mm AM/PM 12-hour format
  • start_time_formatted_datetime (string): Local date and time formatted as MM/DD/YYYY HH:MM AM/PM
  • is_daytime (boolean): Whether this is a daytime period
  • temperature (number): Temperature as an integer
  • temperature_unit (string): "F" for Fahrenheit
  • wind_speed (string): Wind speed (e.g., "10 mph")
  • wind_direction (string): Cardinal wind direction (e.g., "NW")
  • short_forecast (string): Brief condition summary (e.g., "Mostly cloudy")
  • probability_of_precipitation (number | null): Precipitation chance (0–100)
  • relative_humidity (number | null): Relative humidity (0–100)

NWS Hourly Forecast Endpoint

Endpoint: /api/nws-forecast

Method: GET

Authentication: Required — pass a valid x-api-token header (see below).

Headers:

  • x-api-token (required): API token for authentication

Query Parameters:

  • lat (required): Latitude coordinate (US locations only)
  • lon (required): Longitude coordinate (US locations only)

Example Request:

curl -H "x-api-token: your_token_here" \
  "http://localhost:3000/api/nws-forecast?lat=39.7684&lon=-86.1581"

Response:

{
  "periods": [
    {
      "start_time": "2026-02-27T12:00:00-05:00",
      "start_time_formatted_time": "12:00",
      "start_time_formatted_datetime": "02/27/2026 12:00 PM",
      "is_daytime": true,
      "temperature": 45,
      "temperature_unit": "F",
      "wind_speed": "10 mph",
      "wind_direction": "NW",
      "short_forecast": "Mostly cloudy",
      "probability_of_precipitation": 20,
      "relative_humidity": 65
    }
  ]
}
  • periods (array): Up to 12 hourly forecast periods, each with:
    • start_time (string): Full ISO 8601 timestamp of the period start
    • start_time_formatted_time (string): Local time of the period in hh:mm AM/PM 12-hour format
    • start_time_formatted_datetime (string): Local date and time formatted as MM/DD/YYYY HH:MM AM/PM
    • is_daytime (boolean): Whether this is a daytime period
    • temperature (number): Temperature as an integer
    • temperature_unit (string): "F" for Fahrenheit
    • wind_speed (string): Wind speed (e.g., "10 mph")
    • wind_direction (string): Cardinal wind direction (e.g., "NW")
    • short_forecast (string): Brief condition summary (e.g., "Mostly cloudy")
    • probability_of_precipitation (number | null): Precipitation chance (0–100)
    • relative_humidity (number | null): Relative humidity (0–100)

Note: Both NWS endpoints use the National Weather Service API which only covers US locations. No API key required — NWS data is free and public.

Runway Wind Endpoint

Endpoint: /api/runway-wind

Method: GET

Authentication: None required — this endpoint is publicly accessible without an x-api-token header.

Combines an airport's runway layout with current wind data to score how favorable each runway end is to land on or depart from, and can render the result as JSON, an SVG diagram, or a PNG diagram.

Query Parameters:

  • id (optional): Airport ICAO identifier (e.g., KUMP, KJFK). Default: KUMP
  • format (optional): json, svg, or png. Default: json
  • theme (optional, format=svg or format=png only): light or dark. Default: light

Example Request (JSON):

curl "http://localhost:3000/api/runway-wind?id=KJFK"

Response:

{
  "airport_id": "KJFK",
  "airport_name": "John F Kennedy International Airport",
  "wind_direction_degrees": 270,
  "wind_speed_knots": 12,
  "best_runway_identifier": "27",
  "runways": [
    {
      "name": "09/27",
      "length_feet": 14511,
      "width_feet": 150,
      "surface": "Asphalt",
      "ends": [
        {
          "identifier": "09",
          "heading_degrees": 90,
          "latitude": 40.6636,
          "longitude": -73.7967,
          "crosswind_knots": 12,
          "headwind_knots": -0.1,
          "wind_angle_degrees": 180,
          "favorability": "not_favorable"
        },
        {
          "identifier": "27",
          "heading_degrees": 270,
          "latitude": 40.6698,
          "longitude": -73.7659,
          "crosswind_knots": 0.1,
          "headwind_knots": 12,
          "wind_angle_degrees": 0,
          "favorability": "very_favorable"
        }
      ]
    }
  ]
}
  • airport_id (string): Airport ICAO identifier
  • airport_name (string): Airport name
  • wind_direction_degrees (number): Current wind direction in degrees (0–360)
  • wind_speed_knots (number): Current wind speed in knots
  • best_runway_identifier (string): Identifier of the runway end with the most favorable wind
  • runways (array): Each runway's name, length_feet, width_feet, surface, and its two ends
  • Each runway end includes its identifier, true heading_degrees, latitude/longitude, crosswind_knots, headwind_knots (negative indicates a tailwind component), wind_angle_degrees (0–180, angle off the nose), and favorability ("not_favorable", "favorable", or "very_favorable")

Example Request (SVG diagram):

curl "http://localhost:3000/api/runway-wind?id=KJFK&format=svg" --output kjfk-runways.svg
curl "http://localhost:3000/api/runway-wind?id=KJFK&format=svg&theme=dark" --output kjfk-runways-dark.svg

Returns an image/svg+xml compass-rose diagram of the airport, scalable to any size without quality loss. Same layout and color coding as the PNG diagram below.

Example Request (PNG diagram):

curl "http://localhost:3000/api/runway-wind?id=KJFK&format=png" --output kjfk-runways.png
curl "http://localhost:3000/api/runway-wind?id=KJFK&format=png&theme=dark" --output kjfk-runways-dark.png

Returns an image/png compass-rose diagram of the airport with each runway drawn between its two ends, color-coded by wind favorability (gray = not favorable, blue = favorable, green = very favorable). theme=dark renders on a dark background for night use or dark-mode UIs; light (the default) renders on a light background.

LLM Tools Endpoint

Endpoint: /api/tools

Method: GET

Authentication: None required — this endpoint is publicly accessible without an x-api-token header.

Returns Anthropic-compatible tool definitions for all weather endpoints. Use this with the Anthropic API to give a Claude model access to live weather data.

Example Request:

curl "http://localhost:3000/api/tools"

Response:

{
  "tools": [
    {
      "name": "get_current_weather",
      "description": "Get current weather conditions and a daily summary for a location...",
      "input_schema": {
        "type": "object",
        "properties": {
          "latitude": {
            "type": "string",
            "description": "Latitude of the location (e.g. \"39.7684\")"
          },
          "longitude": {
            "type": "string",
            "description": "Longitude of the location (e.g. \"-86.1581\")"
          },
          "units": {
            "type": "string",
            "description": "...",
            "enum": ["metric", "imperial", "standard"]
          }
        },
        "required": ["latitude", "longitude"]
      }
    },
    { "name": "get_aviation_metar", "...": "..." },
    { "name": "get_nws_current_conditions", "...": "..." },
    { "name": "get_nws_hourly_forecast", "...": "..." }
  ]
}

Available tools:

Tool name Wraps endpoint Required inputs
get_current_weather /api/weather latitude, longitude (optional: units)
get_aviation_metar /api/metar none (optional: id)
get_nws_current_conditions /api/nws-current latitude, longitude
get_nws_hourly_forecast /api/nws-forecast latitude, longitude

Usage with Anthropic API:

Fetch the tool definitions and pass them directly to the tools parameter of a Claude API request. When Claude calls a tool, proxy the input object as query parameters to the corresponding endpoint (adding your x-api-token header for authenticated endpoints).

Weather Endpoint

Endpoint: /api/weather

Method: GET

Authentication: Required — pass a valid x-api-token header (see below).

Headers:

  • x-api-token (required): API token for authentication

Query Parameters:

  • lat (required): Latitude coordinate
  • lon (required): Longitude coordinate
  • units (optional): Units of measurement (standard, metric, or imperial). Default: metric

Example Request:

curl -H "x-api-token: your_token_here" \
  "http://localhost:3000/api/weather?lat=40.7128&lon=-74.0060&units=imperial"

Response:

{
  "icon": "23°",
  "message": "Today: High 28°, low 18°, partly cloudy",
  "title": "23° and scattered clouds. Feels like 20°.",
  "temperature": 23
}
  • icon (string): Temperature with degree symbol for display
  • message (string): Today's forecast with high, low, and conditions
  • title (string): Current conditions summary with feels-like temperature
  • temperature (number): Current temperature as an integer (rounded)

Weather by Zip Code Endpoint

Endpoint: /api/zip

Method: GET

Authentication: Required — pass a valid x-api-token header (see below).

Headers:

  • x-api-token (required): API token for authentication

Query Parameters:

  • zip (required): Zip or postal code
  • country (optional): ISO 3166 country code. Default: US
  • units (optional): Units of measurement (standard, metric, or imperial). Default: metric

Example Request:

curl -H "x-api-token: your_token_here" \
  "http://localhost:3000/api/zip?zip=10001&units=imperial"

Response: Same shape as /api/weather — the zip code is resolved to coordinates via OpenWeatherMap's Geocoding API before fetching weather data.

Deployment

Deploy to Vercel:

vercel

Make sure to add both the OPENWEATHERMAP_API_KEY and API_TOKEN environment variables in your Vercel project settings.

About

A Vercel API that queries multiple weather sources. Generated using GitHub Copilot and Claude Code.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages