TL;DR: I built a weather app for pilots that fetches METAR/TAF data from aviationweather.gov and provides VFR flying assessments for Cessna 172 aircraft. The app tells you if conditions are GO, CAUTION, or NO-GO based on real aviation minimums.
The Problem
I rebuilt my old PHP weather app using Next.js 16 and decided to take it further. As a student pilot, I wanted an app that would tell me if the weather was suitable for VFR flight in a Cessna 172. Not just "it's cloudy" but actual aviation weather data: METAR observations, TAF forecasts, and a clear GO/CAUTION/NO-GO assessment.
Existing aviation weather sites show raw data. I wanted something that would interpret that data against Cessna 172 operating limits and give me an immediate answer.
What I Built
The app has two modes:
| Mode | Data Source | Purpose |
|---|---|---|
| Weather | OpenWeatherMap | General weather for any city |
| Aviation | aviationweather.gov | METAR/TAF with VFR assessment |
Aviation mode is the default. Enter an airport code (CYYZ), city name (Toronto), or airport name and get:
- VFR Assessment: GO (green), CAUTION (yellow), or NO-GO (red)
- Flight Category: VFR, MVFR, IFR, or LIFR
- Decoded METAR: Temperature, wind, visibility, ceiling, altimeter
- Raw TAF: Terminal aerodrome forecast for the next 24 hours
- Factor Breakdown: What's driving the assessment
Cessna 172 VFR Limits
The assessment logic uses real C172 operating limits:
| Factor | GO | CAUTION | NO-GO |
|---|---|---|---|
| Ceiling | 3000+ ft | 1500-3000 ft | <1500 ft |
| Visibility | 5+ SM | 3-5 SM | <3 SM |
| Wind | <15 kts | 15-20 kts | >20 kts |
| Crosswind | <12 kts | 12-15 kts | >15 kts |
| Gusts | <20 kts | 20-25 kts | >25 kts |
Precipitation and thunderstorms also factor in. Any NO-GO factor makes the overall assessment NO-GO.
Technical Implementation
Aviation API Route
The app proxies requests to aviationweather.gov to keep the architecture clean:
// app/api/aviation/route.ts
const AVIATION_WEATHER_BASE = 'https://aviationweather.gov/api/data';
async function fetchMetar(icao: string): Promise<MetarResponse | null> {
const url = `${AVIATION_WEATHER_BASE}/metar?ids=${icao}&format=json`;
const response = await fetch(url, {
next: { revalidate: 300 }, // Cache for 5 minutes
});
const data = await response.json();
return data[0] || null;
}
aviationweather.gov is free, requires no API key, and includes Canadian airports. The JSON format makes parsing straightforward.
VFR Assessment Logic
Each weather factor gets assessed independently:
function assessCeiling(clouds: CloudLayer[]): FactorAssessment {
const ceiling = getCeilingFromClouds(clouds);
if (ceiling === null) {
return { status: 'GO', message: 'Clear or no ceiling' };
}
if (ceiling >= 3000) {
return { status: 'GO', value: ceiling, message: `${ceiling} ft AGL` };
}
if (ceiling >= 1500) {
return { status: 'CAUTION', value: ceiling, message: `${ceiling} ft AGL (marginal)` };
}
return { status: 'NO-GO', value: ceiling, message: `${ceiling} ft AGL (too low)` };
}
The overall status is the worst of all factors. One NO-GO means NO-GO.
Flexible Airport Search
The search accepts multiple input formats:
export function normalizeToIcao(input: string): string | null {
const trimmed = input.trim().toUpperCase();
// Already an ICAO code?
if (/^[A-Z]{4}$/.test(trimmed)) {
return trimmed;
}
// Search by city or airport name
const matches = searchAirports(input);
return matches.length > 0 ? matches[0].icao : null;
}
Type "Toronto" and it finds CYYZ. Type "Buttonville" and it finds CYKZ.
Dynamic Backgrounds
The background gradient changes based on assessment:
export function getAviationGradient(status: AssessmentStatus): string {
switch (status) {
case 'GO':
return 'from-emerald-500 via-green-600 to-teal-700';
case 'CAUTION':
return 'from-amber-400 via-orange-500 to-yellow-600';
case 'NO-GO':
return 'from-red-500 via-rose-600 to-red-800';
}
}
You know the assessment before reading a single word.
Architecture
weather-app/
├── app/
│ ├── api/
│ │ ├── weather/route.ts # OpenWeatherMap proxy
│ │ └── aviation/route.ts # aviationweather.gov proxy
│ └── page.tsx # Main page with mode toggle
├── components/
│ ├── airport-search.tsx # ICAO/city/name search
│ ├── vfr-assessment.tsx # GO/CAUTION/NO-GO card
│ ├── metar-display.tsx # Decoded METAR/TAF
│ ├── aviation-display.tsx # Aviation mode container
│ └── ... # Weather mode components
└── lib/
├── aviation-types.ts # TypeScript types
└── aviation-utils.ts # VFR assessment logic
Results
Current conditions as I write this:
| Airport | Assessment | Flight Category | Reason |
|---|---|---|---|
| CYYZ (Toronto) | CAUTION | MVFR | 2100 ft ceiling, light snow |
| CYVR (Vancouver) | GO | VFR | 8500 ft ceiling, clear |
The app correctly identifies marginal conditions and provides actionable recommendations.
Tech Stack
| Technology | Purpose |
|---|---|
| Next.js 16 | App Router, API routes |
| React 19 | UI components |
| TypeScript 5 | Type safety |
| Tailwind CSS 4 | Styling |
| Framer Motion | Animations |
| aviationweather.gov | METAR/TAF data |
Try It
Live Demo: weather-app-five-ivory-48.vercel.app
Repository: github.com/mateenali66/Weather-forecast-openweather
The aviation mode loads by default. Enter any Canadian airport code (CYYZ, CYKZ, CYVR) or search by city name.
Lessons Learned
aviationweather.gov is underrated. Free, no auth, JSON format, global coverage including Canada.
VFR limits are well-defined. The Cessna 172 POH and Transport Canada regulations give clear minimums. Translating them to code was straightforward.
Traffic light UX works. GO/CAUTION/NO-GO is immediately understood. No pilot needs to interpret a dashboard.
Type safety matters for aviation. TypeScript caught several unit mismatches (feet vs meters, knots vs km/h) during development.
Canadian airports use ICAO codes. CYYZ not YYZ. The "C" prefix is required.
Top comments (0)