A TypeScript-based Vercel serverless API that fetches weather data from OpenWeatherMap's 3.0 One Call API and aviation METAR data from Garmin.
This project uses a clean, modular architecture:
- Base Class Pattern: All endpoints extend
ApiEndpointwhich 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
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)
To add a new endpoint:
-
Create a service class in
services/for external API communication -
Define your models in
models/[category]/ -
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 } }
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/metarendpoint 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).
-
Copy
.env.exampleto.env.local:cp .env.example .env.local
-
Add your OpenWeatherMap API key and API token to
.env.local:OPENWEATHERMAP_API_KEY=your_actual_api_key API_TOKEN=your_secure_random_token -
Get an API key from OpenWeatherMap (requires One Call API 3.0 subscription)
Install dependencies:
npm installInstall Vercel CLI if you haven't already:
npm i -g vercelRun the development server:
npm startThe TypeScript files will be automatically compiled by Vercel during development and deployment.
This project uses a comprehensive testing and linting infrastructure to ensure code quality:
# 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:uiTest Coverage: The project maintains 80%+ test coverage for lines/functions and 75%+ for branches, verified on every build.
# 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-checkThe 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.
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.
- 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
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 |
Endpoint: /api/local
Method: GET
Authentication: None required — this endpoint is publicly accessible without an
x-api-tokenheader.
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:indianapolisindianapolisminneapolissan_antoniomilwaukee
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 aftercurrent(same shape as/api/nws-forecast, just fewer periods)- An unrecognized
cityreturns a 500 with a message listing the supported cities
Endpoint: /api/metar
Method: GET
Authentication: None required — this endpoint is publicly accessible without an
x-api-tokenheader.
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 identifierraw_text(string): Full raw METAR stringobservation_time(string): Local observation time (e.g.,"15:51 L")temperature(number): Temperature in °Cdewpoint(number): Dewpoint temperature in °Cwind.direction(number): Wind direction in degrees (0–360)wind.speed(number): Wind speed in knotsvisibility(number): Visibility in statute miles (SM)altimeter(number): Altimeter setting in inHgflight_category(string):"VFR","MVFR","IFR", or"LIFR"sky_conditions(array): Cloud layers, each withcoverage,base_feet, anddescription
Endpoint: /api/nws-current
Method: GET
Authentication: Required — pass a valid
x-api-tokenheader (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 inhh:mm AM/PM12-hour formatstart_time_formatted_datetime(string): Local date and time formatted asMM/DD/YYYY HH:MM AM/PMis_daytime(boolean): Whether this is a daytime periodtemperature(number): Temperature as an integertemperature_unit(string):"F"for Fahrenheitwind_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)
Endpoint: /api/nws-forecast
Method: GET
Authentication: Required — pass a valid
x-api-tokenheader (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 startstart_time_formatted_time(string): Local time of the period inhh:mm AM/PM12-hour formatstart_time_formatted_datetime(string): Local date and time formatted asMM/DD/YYYY HH:MM AM/PMis_daytime(boolean): Whether this is a daytime periodtemperature(number): Temperature as an integertemperature_unit(string):"F"for Fahrenheitwind_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.
Endpoint: /api/runway-wind
Method: GET
Authentication: None required — this endpoint is publicly accessible without an
x-api-tokenheader.
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:KUMPformat(optional):json,svg, orpng. Default:jsontheme(optional,format=svgorformat=pngonly):lightordark. 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 identifierairport_name(string): Airport namewind_direction_degrees(number): Current wind direction in degrees (0–360)wind_speed_knots(number): Current wind speed in knotsbest_runway_identifier(string): Identifier of the runway end with the most favorable windrunways(array): Each runway'sname,length_feet,width_feet,surface, and its twoends- Each runway end includes its
identifier, trueheading_degrees,latitude/longitude,crosswind_knots,headwind_knots(negative indicates a tailwind component),wind_angle_degrees(0–180, angle off the nose), andfavorability("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.svgReturns 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.pngReturns 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.
Endpoint: /api/tools
Method: GET
Authentication: None required — this endpoint is publicly accessible without an
x-api-tokenheader.
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).
Endpoint: /api/weather
Method: GET
Authentication: Required — pass a valid
x-api-tokenheader (see below).
Headers:
x-api-token(required): API token for authentication
Query Parameters:
lat(required): Latitude coordinatelon(required): Longitude coordinateunits(optional): Units of measurement (standard,metric, orimperial). 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 displaymessage(string): Today's forecast with high, low, and conditionstitle(string): Current conditions summary with feels-like temperaturetemperature(number): Current temperature as an integer (rounded)
Endpoint: /api/zip
Method: GET
Authentication: Required — pass a valid
x-api-tokenheader (see below).
Headers:
x-api-token(required): API token for authentication
Query Parameters:
zip(required): Zip or postal codecountry(optional): ISO 3166 country code. Default:USunits(optional): Units of measurement (standard,metric, orimperial). 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.
Deploy to Vercel:
vercelMake sure to add both the OPENWEATHERMAP_API_KEY and API_TOKEN environment variables in your Vercel project settings.