0%

AI-Powered Weather App Development

Web Development 👤 Nicolás Deyros ⏱️ 18 min read

Building a Production-Ready Weather App: A Journey with AI-Powered Development

How I built a comprehensive weather application using Astro, TypeScript, and Tailwind CSS with the help of AI assistants - and what I learned along the way.


The Beginning: A Simple Idea

It started as many projects do - with a simple need. I wanted to build a modern weather application that would showcase real-world development practices, from initial concept to production deployment. But this wasn’t going to be just another tutorial project. I wanted to explore something exciting: AI-assisted development.

Armed with access to two cutting-edge AI assistants - GitHub Copilot and Jules (Google’s experimental AI) - I embarked on a journey that would teach me as much about collaborative AI development as it would about building great software.

💡 Key Insight

This wasn’t just about building a weather app - it was about exploring a new paradigm of development where AI assistants become collaborative partners in the creative process.

The Tech Stack: Modern and Minimal

Before diving into code, I needed to choose the right tools. After some consideration, I settled on:

🚀 Astro 5.12.6

For its islands architecture and excellent performance

📘 TypeScript

For type safety and better developer experience

🎨 Tailwind CSS 4.1.11

For rapid, utility-first styling

🌤️ Open-Meteo API

For reliable, free weather data

⚡ Vercel

For seamless deployment and serverless functions

🤖 AI Assistants

GitHub Copilot & Google Jules

The beauty of this stack is its simplicity. No complex state management, no unnecessary abstractions - just modern web technologies working together harmoniously.

Chapter 1: Building the Foundation

The Core Architecture

The first major decision was how to structure the application. I chose Astro’s file-based routing with a server-side rendering approach:

This API endpoint became the backbone of the application. Notice how I implemented proper error handling from the start - a practice that would prove crucial as the app grew in complexity.

The Weather Data Transformation

One of the most interesting challenges was transforming the Open-Meteo API response into something more usable. The raw API returns weather codes (integers) that need to be mapped to human-readable descriptions and appropriate icons:

// src/utils/weatherMapping.ts
export const WEATHER_CODE_MAPPING = {
	0: { icon: 'meteocons:clear-day-fill', description: 'Clear sky' },
	1: { icon: 'meteocons:partly-cloudy-day-fill', description: 'Mainly clear' },
	2: { icon: 'meteocons:partly-cloudy-day-fill', description: 'Partly cloudy' },
	3: { icon: 'meteocons:overcast-fill', description: 'Overcast' },
	45: { icon: 'meteocons:fog-fill', description: 'Fog' },
	48: { icon: 'meteocons:fog-fill', description: 'Depositing rime fog' },
	51: { icon: 'meteocons:drizzle-fill', description: 'Light drizzle' },
	// ... 50+ more mappings
} as const

This mapping system proved to be one of the most valuable parts of the application - it centralized all weather interpretation logic and made testing much easier.

Chapter 2: The AI Collaboration Begins

Working with GitHub Copilot

🤖

GitHub Copilot

Code Completion Specialist

Strengths: Pattern recognition, code completion, repetitive tasks

GitHub Copilot became my constant companion during the development process. Its suggestions were particularly brilliant when working on repetitive patterns. For example, when building the weather icon animations, Copilot helped me create a comprehensive set of CSS keyframes:

/* AI-assisted animation creation */
@keyframes rainDrop {
	0%,
	100% {
		transform: translateY(0px);
	}
	50% {
		transform: translateY(-10px);
	}
}

@keyframes snowFloat {
	0%,
	100% {
		transform: translateY(0px) rotate(0deg);
	}
	33% {
		transform: translateY(-8px) rotate(120deg);
	}
	66% {
		transform: translateY(-4px) rotate(240deg);
	}
}

@keyframes thunderPulse {
	0%,
	100% {
		transform: scale(1);
		opacity: 1;
	}
	25% {
		transform: scale(1.1);
		opacity: 0.8;
	}
	50% {
		transform: scale(1.05);
		opacity: 1;
	}
	75% {
		transform: scale(1.15);
		opacity: 0.7;
	}
}

Copilot didn’t just suggest code - it understood the context and helped me maintain consistency across all animations.

The Jules (Google AI) Experience

🧠

Jules (Google AI)

Strategic Architecture Advisor

Strengths: Strategic guidance, debugging, architectural decisions

Working with Jules brought a different perspective to the development process. Where Copilot excelled at code completion and pattern recognition, Jules provided more strategic guidance on architecture decisions and debugging complex issues.

When I encountered a tricky bug where weather icons weren’t displaying properly, Jules helped me trace through the entire data flow:

  1. API Response Analysis - Verified the Open-Meteo API was returning correct data

  2. Data Transformation - Checked that weather codes were being mapped correctly

  3. Icon Resolution - Identified that the Astro Icon component needed proper fallbacks

  4. Client-Side Rendering - Discovered issues with hydration timing

This systematic approach led to implementing a robust fallback system:

// Emoji-based fallback system for reliable icon display
function createWeatherIcon(iconName: string, isLarge: boolean = false): string {
	const sizeClass = isLarge ? 'text-8xl' : 'text-4xl'
	let emoji = '☁️'
	let animationClass = ''

	switch (iconName) {
		case 'meteocons:clear-day-fill':
			emoji = '☀️'
			animationClass = 'animate-bounce'
			break
		case 'meteocons:rain-fill':
			emoji = '🌧️'
			animationClass = 'animate-rain-drop'
			break
		// Smart pattern matching for unknown icons
		default:
			if (iconName.includes('clear') && iconName.includes('day')) {
				emoji = '☀️'
				animationClass = 'animate-bounce'
			} else if (iconName.includes('rain')) {
				emoji = '🌧️'
				animationClass = 'animate-rain-drop'
			}
		// ... more intelligent fallbacks
	}

	return `<span class="${sizeClass} ${animationClass} inline-block hover:scale-110 transition-all duration-300 cursor-pointer" title="${iconName}">${emoji}</span>`
}

Chapter 3: The Iteration Cycles

First Iteration: Basic Functionality

The first working version was surprisingly straightforward:

  • Simple location input
  • Basic weather display
  • Minimal styling

But as I started using it, the limitations became apparent. The user experience was clunky, error handling was minimal, and the design felt amateurish.

Second Iteration: Enhanced UX

This is where the AI collaboration really shone. Both Copilot and Jules helped me identify UX improvements:

Quick Location Buttons:

<!-- AI-suggested quick access buttons -->
<div class="grid grid-cols-2 gap-3 sm:grid-cols-3">
	<button
		data-lat="51.5074"
		data-lon="-0.1278"
		data-city="London"
		class="rounded-lg bg-blue-600 px-4 py-3 text-white hover:bg-blue-700">
		🇬🇧 London
	</button>
	<button
		data-lat="40.7128"
		data-lon="-74.0060"
		data-city="New York"
		class="rounded-lg bg-blue-600 px-4 py-3 text-white hover:bg-blue-700">
		🇺🇸 New York
	</button>
	<!-- More cities... -->
</div>

Geolocation Integration:

// AI-assisted geolocation with proper error handling
async function getCurrentLocation() {
	if (!navigator.geolocation) {
		throw new Error('Geolocation is not supported by this browser')
	}

	return new Promise((resolve, reject) => {
		const options = {
			enableHighAccuracy: true,
			timeout: 15000,
			maximumAge: 300000, // 5 minutes
		}

		navigator.geolocation.getCurrentPosition(
			position => {
				const { latitude, longitude } = position.coords
				resolve({ lat: latitude, lon: longitude })
			},
			error => {
				const errorMessages = {
					1: 'Location access denied by user',
					2: 'Location information is unavailable',
					3: 'Location request timed out',
				}
				reject(new Error(errorMessages[error.code] || 'Unknown location error'))
			},
			options,
		)
	})
}

Third Iteration: Production Polish

The final iteration focused on production readiness. This meant:

  1. Comprehensive Error Handling
  2. Performance Optimization
  3. Accessibility Improvements
  4. Mobile Responsiveness
  5. SEO Optimization

Chapter 4: The Testing Revolution

AI-Assisted Test Development

One of the most impressive aspects of working with AI was how it helped me build a comprehensive test suite. Starting with utility tests:

// src/test/utils.test.ts - AI helped structure comprehensive tests
import { describe, it, expect } from 'vitest'
import {
	getWeatherDescription,
	getWeatherIcon,
	convertToFahrenheit,
	convertToCelsius,
} from '../utils/weatherMapping'

describe('Weather Utilities', () => {
	describe('Temperature Conversion', () => {
		it('should convert Celsius to Fahrenheit correctly', () => {
			expect(convertToFahrenheit(0)).toBe(32)
			expect(convertToFahrenheit(25)).toBe(77)
			expect(convertToFahrenheit(-10)).toBe(14)
		})

		it('should convert Fahrenheit to Celsius correctly', () => {
			expect(convertToCelsius(32)).toBe(0)
			expect(convertToCelsius(77)).toBe(25)
			expect(convertToCelsius(14)).toBe(-10)
		})
	})

	describe('Weather Code Mapping', () => {
		it('should return correct descriptions for all weather codes', () => {
			expect(getWeatherDescription(0)).toBe('Clear sky')
			expect(getWeatherDescription(3)).toBe('Overcast')
			expect(getWeatherDescription(95)).toBe('Thunderstorm')
		})

		it('should handle invalid weather codes gracefully', () => {
			expect(getWeatherDescription(999)).toBe('Unknown weather condition')
		})
	})
})

Integration Testing

The AI assistants helped me build integration tests that actually start the development server and test the full application flow:

// src/test/frontend.test.ts
import { describe, it, expect, beforeAll, afterAll } from 'vitest'
import { startServerAndTest } from 'start-server-and-test'

describe('Frontend Integration', () => {
	const baseUrl = 'http://localhost:3001'

	it('should load the main page successfully', async () => {
		const response = await fetch(baseUrl)
		expect(response.status).toBe(200)

		const html = await response.text()
		expect(html).toContain('Weather')
		expect(html).toContain('App')
	})

	it('should display weather data when coordinates are provided', async () => {
		const response = await fetch(`${baseUrl}?lat=51.5074&lon=-0.1278`)
		const html = await response.text()

		expect(html).toContain('London')
		expect(html).toContain('°C')
		expect(html).toContain('Next 3 Days')
	})
})

The final test suite included 40 passing tests covering:

  • 11 utility tests - Temperature conversion, weather mapping, data validation
  • 14 API tests - Endpoint responses, error handling, data transformation
  • 15 frontend tests - Component rendering, user interactions, responsive design

Chapter 5: The Developer Experience Revolution

Tooling Setup with AI Guidance

One of the most time-consuming aspects of modern development is setting up proper tooling. Here’s where the AI collaboration truly excelled. Instead of spending hours configuring ESLint, Stylelint, Prettier, and Git hooks, the AI assistants helped me create a production-ready setup in minutes.

ESLint Configuration:

// eslint.config.js - AI-optimized configuration
import astro from 'eslint-plugin-astro'
import typescriptEslint from 'typescript-eslint'
import simpleImportSort from 'eslint-plugin-simple-import-sort'

export default [
	...typescriptEslint.configs.strict,
	...astro.configs.recommended,
	{
		plugins: {
			'simple-import-sort': simpleImportSort,
		},
		rules: {
			'simple-import-sort/imports': 'error',
			'simple-import-sort/exports': 'error',
			'@typescript-eslint/no-unused-vars': [
				'error',
				{
					argsIgnorePattern: '^_',
					varsIgnorePattern: '^_',
				},
			],
		},
	},
]

Stylelint for Tailwind CSS:

{
	"extends": ["stylelint-config-standard"],
	"rules": {
		"at-rule-no-unknown": [
			true,
			{
				"ignoreAtRules": [
					"tailwind",
					"apply",
					"variants",
					"responsive",
					"screen",
					"layer"
				]
			}
		],
		"no-descending-specificity": null,
		"selector-class-pattern": null,
		"custom-property-pattern": null
	},
	"ignoreFiles": [
		"dist/**/*",
		".vercel/**/*",
		".astro/**/*",
		"node_modules/**/*"
	]
}

Pre-commit Hooks with Husky and Lint-staged:

{
	"lint-staged": {
		"*.{js,ts,astro,jsx,tsx}": ["eslint --fix"],
		"*.css": ["stylelint --fix"],
		"*.{js,ts,astro,jsx,tsx,css,json,md}": ["prettier --write"]
	}
}

This setup meant that every commit was automatically:

  • Linted for code quality issues
  • Formatted consistently
  • Tested to ensure nothing was broken
  • Validated for commit message format

The Power of Automated Scripts

The AI assistants helped me create a comprehensive set of npm scripts that made development a breeze:

{
	"scripts": {
		"dev": "astro dev --port 3001 --open",
		"build": "astro build",
		"lint:all": "npm run lint && npm run lint:css",
		"test:all": "npm run test:unit && npm run test:integration",
		"test:integration": "start-server-and-test dev http://localhost:3001 \"vitest run src/test/api.test.ts src/test/frontend.test.ts\"",
		"test:coverage": "vitest --coverage src/test/utils.test.ts"
	}
}

The test:integration script was particularly clever - it automatically starts the development server, waits for it to be ready, runs the integration tests, then shuts down the server. No manual server management required!

Chapter 6: Advanced Features and Polish

Smart Icon System

One of the features I’m most proud of is the intelligent weather icon system. Instead of relying solely on SVG icons (which can fail to load), I implemented a dual-system approach:

// Primary: SVG icons with Astro Icon component
<Icon name={weatherData.current.icon} class="h-32 w-32 text-white drop-shadow-lg" />

// Fallback: Animated emoji system
function createWeatherIcon(iconName: string, isLarge: boolean = false): string {
  const sizeClass = isLarge ? 'text-8xl' : 'text-4xl'
  let emoji = '☁️'
  let animationClass = ''

  // Comprehensive mapping with intelligent pattern matching
  switch (iconName) {
    case 'meteocons:clear-day-fill':
      emoji = '☀️'
      animationClass = 'animate-bounce'
      break
    case 'meteocons:thunderstorms-fill':
      emoji = '⛈️'
      animationClass = 'animate-thunder-pulse'
      break
    // ... 18+ weather conditions covered

    default:
      // Smart fallback based on icon name patterns
      if (iconName.includes('clear') && iconName.includes('day')) {
        emoji = '☀️'
        animationClass = 'animate-bounce'
      } else if (iconName.includes('thunder') || iconName.includes('storm')) {
        emoji = '⛈️'
        animationClass = 'animate-thunder-pulse'
      }
      // ... more intelligent pattern matching
  }

  return `<span class="${sizeClass} ${animationClass} inline-block hover:scale-110 transition-all duration-300 cursor-pointer" title="${iconName}">${emoji}</span>`
}

This system ensures that users always see appropriate weather icons, even if the SVG icon set fails to load.

Temperature Unit Persistence

A small but important UX detail was remembering the user’s temperature unit preference:

// Temperature toggle with localStorage persistence
window.switchToUnit = function (unit) {
	localStorage.setItem('temperatureUnit', unit)

	// Update toggle state
	const toggleCheckbox = document.getElementById('temp-toggle-checkbox')
	if (toggleCheckbox) {
		toggleCheckbox.checked = unit === 'fahrenheit'
	}

	// Convert all displayed temperatures
	document.querySelectorAll('[data-temp-c]').forEach(element => {
		const celsius = parseFloat(element.dataset.tempC)
		const converted =
			unit === 'fahrenheit' ? Math.round((celsius * 9) / 5 + 32) : celsius
		const suffix = unit === 'fahrenheit' ? '°F' : '°C'
		element.textContent = `${converted}${suffix}`
	})
}

// Restore user preference on page load
const savedUnit = localStorage.getItem('temperatureUnit') || 'celsius'
window.switchToUnit(savedUnit)

Responsive Design Excellence

The application needed to work perfectly on all devices. Tailwind CSS made this straightforward, but the AI assistants helped me think through the user experience:

<!-- Desktop: Two-column layout, Mobile: Single column -->
<div class="grid grid-cols-1 gap-8 lg:grid-cols-2">
	<!-- Left Column: Search and Location Buttons -->
	<div class="space-y-8">
		<div class="mx-auto max-w-lg lg:mx-0 lg:max-w-none">
			<LocationSearch />
		</div>
		<div class="mx-auto max-w-lg lg:mx-0 lg:max-w-none">
			<Location />
		</div>
	</div>

	<!-- Right Column: Weather Display -->
	<div class="weather-container">
		<!-- Weather content adapts automatically -->
	</div>
</div>

Chapter 7: Deployment and Production

Vercel Integration

Deploying to Vercel was remarkably smooth thanks to Astro’s built-in adapter:

// astro.config.mjs
import { defineConfig } from 'astro/config'
import tailwindcss from '@tailwindcss/vite'
import icon from 'astro-icon'
import vercel from '@astrojs/vercel'

export default defineConfig({
	vite: {
		plugins: [tailwindcss()],
	},
	integrations: [icon()],
	adapter: vercel(),
})

That’s it! The application automatically builds and deploys with:

  • Server-side rendering for initial page loads
  • Static generation for optimal performance
  • Serverless API routes for weather data
  • Edge caching for global performance

Performance Optimization

The final application achieved excellent performance metrics:

1.2s

First Contentful Paint

✓ Under 1.2s target
2.1s

Largest Contentful Paint

✓ Under 2.5s target
0.08

Cumulative Layout Shift

✓ Under 0.1 target
2.8s

Time to Interactive

✓ Under 3.0s target
🏆

Performance Achievement

All Core Web Vitals passed Google’s “Good” thresholds, resulting in better search rankings and user experience scores.

These scores were achieved through:

  • Astro’s islands architecture - Minimal JavaScript footprint
  • Efficient CSS with Tailwind’s purging - Only unused styles removed
  • Optimized weather API calls - Smart caching and request batching
  • Proper image optimization - WebP format with fallbacks
  • Strategic use of SSR vs static generation - Best of both worlds

Chapter 8: Lessons Learned

AI as a Development Partner

Working with AI assistants fundamentally changed how I approach development:

✅ What AI Excels At

  • Pattern Recognition - Spotting repetitive code and suggesting abstractions

  • Boilerplate Generation - Creating comprehensive configurations and test setups

  • Error Debugging - Systematically working through complex issues

  • Code Consistency - Maintaining style and patterns across the codebase

  • Documentation - Generating thorough comments and README content

⚠️ What AI Struggles With

  • Creative Problem Solving - Novel solutions to unique problems

  • Business Logic Decisions - Understanding user needs and requirements

  • Performance Trade-offs - Making architectural decisions based on constraints

  • User Experience - Understanding subtle UX implications

🎯

The Sweet Spot

The most productive development happened when I provided the creative direction and architectural decisions, while the AI assistants handled implementation details, testing strategies, and tooling configuration.

Technical Insights

📝

TypeScript is Essential

Even for a relatively simple application, TypeScript caught numerous potential runtime errors and made refactoring much safer.

🧪

Testing Pays Off

The comprehensive test suite caught several bugs during development and gave me confidence to refactor aggressively.

🔧

Tooling Investment

Spending time upfront on proper ESLint, Stylelint, and pre-commit hooks paid dividends throughout development.

Performance by Default

Astro’s architecture meant the application was fast without additional optimization effort.

🚀

Progressive Enhancement

Building the core functionality to work without JavaScript, then enhancing with interactivity, created a more robust application.

The Final Product

The completed weather application showcases modern web development best practices:

🌟 Features

  • Global Weather Data - Any location worldwide via Open-Meteo API
  • Responsive Design - Seamless experience across all devices
  • Smart Weather Icons - 18+ animated weather conditions with intelligent fallbacks
  • 3-Day Forecasts - Current conditions plus detailed forecasts
  • Unit Conversion - Celsius/Fahrenheit toggle with preference persistence
  • Location Search - City search with geolocation support
  • Performance Optimized - Fast loading with excellent Core Web Vitals
  • Accessibility Ready - Proper ARIA labels, keyboard navigation, screen reader support

🧪 Quality Assurance

  • 40 Comprehensive Tests - Unit, integration, and API testing
  • 100% TypeScript - Full type safety throughout the application
  • Automated Code Quality - ESLint, Stylelint, Prettier with pre-commit hooks
  • Production Deployment - Live on Vercel with serverless functions

🚀 Performance Metrics

  • Lighthouse Score: 95+ across all categories
  • Bundle Size: < 50KB gzipped
  • First Load: < 1.2s on 3G
  • SEO Optimized: Proper meta tags, structured data, semantic HTML

Reflections on AI-Assisted Development

This project proved that AI isn’t replacing developers - it’s making us more effective. The combination of human creativity and AI efficiency created something neither could have built alone.

The Future of Development:

  • AI handles boilerplate, configuration, and testing setup
  • Developers focus on architecture, user experience, and business logic
  • Code quality improves through AI-assisted best practices
  • Development velocity increases without sacrificing quality

Key Takeaways:

  1. AI is a Force Multiplier - It amplifies good development practices but won’t fix poor ones
  2. Human Judgment Remains Critical - AI suggestions need evaluation and refinement
  3. Iteration is Key - The best results come from multiple AI-assisted refinement cycles
  4. Testing Becomes Essential - AI can generate comprehensive tests that catch issues early
  5. Documentation Quality Improves - AI helps maintain thorough, up-to-date documentation

What’s Next?

The weather app serves as a foundation for exploring more advanced concepts:

  • Real-time Updates - WebSocket connections for live weather changes
  • Offline Support - Service workers and cached data
  • Advanced Visualizations - Weather maps and interactive charts
  • Machine Learning - Personalized weather insights
  • Mobile App - React Native or Flutter implementation

But more importantly, this project demonstrated a new way of building software - one where AI assistants handle the mechanical aspects of development while humans focus on the creative and strategic elements.

The future of web development isn’t about humans vs. AI - it’s about humans with AI, building better software faster than either could alone.


Want to explore the code? Check out the live demo or browse the source code on GitHub. The complete test suite, tooling configuration, and deployment setup are all available for reference.

Final Stats:

  • Lines of Code: ~2,000
  • Development Time: 3 days
  • Tests Written: 40
  • AI Interactions: 200+
  • Coffee Consumed: ☕☕☕

This project was built with the assistance of GitHub Copilot and Jules (Google AI). All code, architecture decisions, and final implementation were human-reviewed and approved.