# How to Build a Weather Application with Lovable

- Tool: How to Build with Lovable
- Difficulty: Beginner
- Compatibility: Lovable Free or higher
- Last updated: April 2026

## TL;DR

Build a weather app in Lovable with a city search using shadcn/ui Command, a 7-day forecast with Recharts, and geolocation support. All weather API calls go through a Supabase Edge Function to keep your API key secure, and responses are cached in Supabase for 30 minutes to stay within free API rate limits.

## Before you start

- Lovable account (Free tier works for this build)
- OpenWeatherMap API key — free at openweathermap.org (free tier: 1,000 calls/day)
- Supabase project with URL and anon key. Store the OpenWeatherMap key in Cloud tab → Secrets as OPENWEATHER_API_KEY

## Step-by-step guide

### 1. Create the weather cache table in Supabase

Prompt Lovable to set up the simple caching table and the Edge Function scaffold. The schema is minimal — this is a beginner project.

```
Build a weather application. Create one Supabase table:

- weather_cache: id, cache_key (text, UNIQUE — format: 'weather:lat:lon' or 'weather:city_name'), response_data (jsonb), cached_at (timestamptz default now())

No user_id or RLS needed — this is a public cache accessible by anyone.

Create an index: CREATE INDEX idx_weather_cache_key ON weather_cache(cache_key).

Set up the app shell with:
- A search bar at the top using shadcn/ui Command component
- A main content area for the weather display
- A 'Use My Location' Button next to the search bar
- A saved cities section below the main content (store in localStorage for simplicity)

The app should use a clean dark or light weather-themed design with sky blue and white as primary colors.
```

> Pro tip: Ask Lovable to add a saved cities feature using localStorage (not Supabase — no auth needed for this app). Users can click a star icon on a city to save it, and saved cities appear as quick-access Badges below the search bar.

**Expected result:** The weather_cache table is created. The app shows a Command search bar, a placeholder weather Card, and a 'Use My Location' Button.

### 2. Create the weather proxy Edge Function

Build the Edge Function that proxies requests to OpenWeatherMap. It checks the cache first, calls the API if cache is stale, and stores the result.

```
// supabase/functions/get-weather/index.ts
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
  'Content-Type': 'application/json',
}

const CACHE_MINUTES = 30
const API_KEY = Deno.env.get('OPENWEATHER_API_KEY') ?? ''

serve(async (req: Request) => {
  if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })

  const supabase = createClient(Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '')
  const url = new URL(req.url)
  const lat = url.searchParams.get('lat')
  const lon = url.searchParams.get('lon')
  const city = url.searchParams.get('city')

  if (!API_KEY) return new Response(JSON.stringify({ error: 'API key not configured' }), { status: 500, headers: corsHeaders })

  const cacheKey = lat && lon ? `weather:${parseFloat(lat).toFixed(2)}:${parseFloat(lon).toFixed(2)}` : `weather:${city?.toLowerCase().trim()}`
  if (!cacheKey.includes(':')) return new Response(JSON.stringify({ error: 'Provide lat+lon or city' }), { status: 400, headers: corsHeaders })

  const staleAfter = new Date(Date.now() - CACHE_MINUTES * 60 * 1000).toISOString()
  const { data: cached } = await supabase.from('weather_cache').select('response_data, cached_at').eq('cache_key', cacheKey).gte('cached_at', staleAfter).single()

  if (cached) {
    return new Response(JSON.stringify({ ...cached.response_data, _cached: true }), { headers: corsHeaders })
  }

  const query = lat && lon ? `lat=${lat}&lon=${lon}` : `q=${encodeURIComponent(city ?? '')}`
  const [currentRes, forecastRes] = await Promise.all([
    fetch(`https://api.openweathermap.org/data/2.5/weather?${query}&appid=${API_KEY}&units=metric`),
    fetch(`https://api.openweathermap.org/data/2.5/forecast?${query}&appid=${API_KEY}&units=metric`),
  ])

  if (!currentRes.ok) {
    const err = await currentRes.json()
    return new Response(JSON.stringify({ error: err.message ?? 'City not found' }), { status: 404, headers: corsHeaders })
  }

  const [current, forecast] = await Promise.all([currentRes.json(), forecastRes.json()])
  const responseData = { current, forecast }

  await supabase.from('weather_cache').upsert({ cache_key: cacheKey, response_data: responseData, cached_at: new Date().toISOString() }, { onConflict: 'cache_key' })

  return new Response(JSON.stringify(responseData), { headers: corsHeaders })
})
```

**Expected result:** The Edge Function deploys. Calling /functions/v1/get-weather?city=London returns current weather and forecast JSON. A second call within 30 minutes returns cached data with _cached: true.

### 3. Add location search with autocomplete

Allow users to search for any city by name. A Command component with debounced input calls a geocoding Edge Function that returns matching city suggestions. Selecting a city updates the weather display for those coordinates.

```
Add city search autocomplete to the weather app. Use the shadcn/ui Command component with a debounced CommandInput (300ms delay). On each input change, call a Supabase Edge Function get-geocoding that hits the OpenWeatherMap Geocoding API (http://api.openweathermap.org/geo/1.0/direct?q={city}&limit=5&appid={key}) and returns an array of { name, country, state, lat, lon }. Display each result as a CommandItem showing '{name}, {state}, {country}'. When a result is selected, store the city name and coordinates in component state and call the get-weather Edge Function with those lat/lon values to update the displayed weather. Show a CommandEmpty state: 'No cities found. Check the spelling and try again.' Show a loading spinner inside the CommandInput while the geocoding request is in flight.
```

> Pro tip: Cache geocoding results in Supabase (a separate geocoding_cache table with city query as key, TTL of 7 days) to avoid hitting rate limits when users search for the same popular city names repeatedly.

**Expected result:** Type a city name and see autocomplete suggestions. Selecting a city updates the weather display for that location.

### 4. Build the weather display with Command search and charts

Ask Lovable to build the main weather UI: the Command search, current conditions Card, and the 7-day forecast chart.

```
Build the weather display UI:

1. City search using shadcn/ui Command component:
   - A CommandInput that calls the OpenWeatherMap geocoding API (also via Edge Function) after 300ms of no typing
   - Show city suggestions as CommandItems with city name and country
   - Selecting a city calls the get-weather Edge Function and updates the displayed weather

2. Current conditions Card:
   - Large temperature display (e.g. 22°C)
   - City name and country as heading
   - Weather description (e.g. 'Partly Cloudy') with an emoji icon based on OpenWeatherMap icon code
   - Grid of metrics: Feels Like, Humidity %, Wind Speed km/h, Visibility km
   - 'Cached' Badge in corner if _cached: true in response

3. 7-day forecast Recharts AreaChart:
   - Parse the forecast API response (it returns 3-hour intervals, group by day, take the noon entry)
   - X-axis: day names (Mon, Tue, Wed...)
   - Two lines: max temperature and min temperature
   - Tooltip showing high/low and description per day
   - Small weather emoji above each data point

4. 'Use My Location' Button:
   - On click: call navigator.geolocation.getCurrentPosition()
   - Pass coords to get-weather Edge Function as lat+lon parameters
   - Show a loading spinner while fetching
```

**Expected result:** Searching for a city shows autocomplete suggestions. Selecting a city loads the current conditions Card and the 7-day forecast chart. The geolocation button fetches weather for the user's current position.

## Complete code example

File: `src/hooks/useWeather.ts`

```typescript
import { useState, useCallback } from 'react'
import { supabase } from '@/integrations/supabase/client'

export interface WeatherData {
  current: {
    name: string
    sys: { country: string }
    main: { temp: number; feels_like: number; humidity: number }
    wind: { speed: number }
    weather: Array<{ description: string; icon: string }>
    visibility: number
  }
  forecast: {
    list: Array<{
      dt_txt: string
      main: { temp_max: number; temp_min: number }
      weather: Array<{ description: string; icon: string }>
    }>
  }
  _cached?: boolean
}

export function useWeather() {
  const [data, setData] = useState<WeatherData | null>(null)
  const [isLoading, setIsLoading] = useState(false)
  const [error, setError] = useState<string | null>(null)

  const fetchWeather = useCallback(async (params: { city?: string; lat?: number; lon?: number }) => {
    setIsLoading(true)
    setError(null)
    try {
      const searchParams = new URLSearchParams()
      if (params.city) searchParams.set('city', params.city)
      if (params.lat !== undefined) searchParams.set('lat', String(params.lat))
      if (params.lon !== undefined) searchParams.set('lon', String(params.lon))

      const { data: result, error: fnError } = await supabase.functions.invoke('get-weather', {
        body: null,
        headers: {},
      })

      const url = `${(supabase as any).supabaseUrl}/functions/v1/get-weather?${searchParams}`
      const res = await fetch(url, {
        headers: { apikey: (supabase as any).supabaseKey },
      })

      if (!res.ok) {
        const err = await res.json()
        throw new Error(err.error ?? 'Failed to fetch weather')
      }

      const weatherData = await res.json()
      setData(weatherData)
    } catch (err) {
      setError(err instanceof Error ? err.message : 'Unknown error')
    } finally {
      setIsLoading(false)
    }
  }, [])

  const fetchByGeolocation = useCallback(() => {
    if (!navigator.geolocation) {
      setError('Geolocation is not supported by your browser')
      return
    }
    setIsLoading(true)
    navigator.geolocation.getCurrentPosition(
      (pos) => fetchWeather({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
      () => { setError('Location access denied'); setIsLoading(false) }
    )
  }, [fetchWeather])

  return { data, isLoading, error, fetchWeather, fetchByGeolocation }
}
```

## Common mistakes

- **Calling the weather API directly from the frontend** — Calling a third-party API directly from React code exposes the API key in the browser's network tab. Anyone can see it, copy it, and use it at your expense. Fix: Always proxy API calls through a Supabase Edge Function. The key is stored in Deno.env.get('OPENWEATHER_API_KEY'), which is only accessible server-side. The frontend never sees the key.
- **Using VITE_OPENWEATHER_API_KEY on the frontend** — VITE_ prefixed variables are embedded in the JavaScript bundle at build time and visible to anyone who views the page source. This is equivalent to hard-coding the key in your code. Fix: Store OPENWEATHER_API_KEY (without VITE_ prefix) in Cloud tab → Secrets for Edge Functions only. The frontend makes requests to your Edge Function URL, never to OpenWeatherMap directly.
- **Not handling the case where geolocation is denied** — If a user denies location permission or is on a browser that doesn't support geolocation, calling navigator.geolocation.getCurrentPosition() without error handling causes an unhandled error. Fix: Always provide a second callback to getCurrentPosition for error handling. Update the UI to show a helpful message: 'Location access was denied. Please search for your city manually.' The useWeather hook in this guide shows the correct error handling pattern.
- **Not grouping the 3-hour forecast intervals into daily summaries** — OpenWeatherMap's free /forecast endpoint returns data in 3-hour intervals (40 entries for 5 days). Rendering all 40 points on a chart creates a cluttered graph that's hard to read. Fix: Group forecast entries by date and take the entry closest to noon (12:00 UTC) as the representative for each day. Extract temp_max and temp_min across all intervals for that day. This gives you clean daily summaries for the chart.

## Best practices

- Always proxy third-party API calls through an Edge Function. Never put API keys in VITE_ variables or frontend code. The weather proxy pattern in this guide applies to any third-party API.
- Cache API responses in Supabase to stay within free tier limits. OpenWeatherMap's free tier allows 1,000 calls/day. With caching, a popular city like 'London' is only fetched once per 30 minutes regardless of how many users request it.
- Show a loading skeleton while weather data is fetching. A skeleton Card with animated pulse (the Skeleton shadcn/ui component) prevents layout shift and makes the app feel faster.
- Handle API errors gracefully with user-friendly messages. 'City not found' is better than a raw JSON error. Map HTTP 404 from OpenWeatherMap to 'City not found. Check the spelling and try again.'
- Round temperature values to integers for display. Showing '22.3°C' vs '22°C' adds false precision — weather forecasts are not that accurate. Use Math.round() before displaying temperatures.
- Add a units toggle (Celsius/Fahrenheit) stored in localStorage. When the unit changes, re-fetch using the units=imperial parameter. Show a Toggle component in the app header for the unit switch.

## Frequently asked questions

### Which weather API should I use with this guide?

This guide uses OpenWeatherMap, which has a free tier with 1,000 API calls per day and no credit card required. The /weather and /forecast endpoints used here are both available on the free tier. Sign up at openweathermap.org, generate an API key, and save it to Cloud tab → Secrets as OPENWEATHER_API_KEY. It takes up to 2 hours for a new key to activate.

### Why use an Edge Function instead of calling OpenWeatherMap directly from React?

Calling a third-party API directly from the browser exposes your API key in the network tab — anyone can see it, copy it, and use it at your expense. An Edge Function proxies the request server-side, keeping the key hidden. The key is stored in Deno.env (Cloud tab → Secrets in Lovable) and never reaches the browser.

### How does the 30-minute cache work?

Before calling OpenWeatherMap, the Edge Function checks the weather_cache table for a row with the same cache_key (city name or coordinates) where cached_at is within the last 30 minutes. If found, it returns the stored JSONB data instantly without an API call. If not found or expired, it calls OpenWeatherMap, stores the response, and returns it. This means a popular city might only trigger one real API call every 30 minutes regardless of how many users request it.

### Can I use a different weather API like WeatherAPI or Tomorrow.io?

Yes. Replace the OpenWeatherMap API calls in the Edge Function with your preferred service. Change the fetch URL and update the response parsing in the frontend to match the new API's schema. The caching and proxy pattern stays exactly the same. Just update the environment variable name in Cloud tab → Secrets to match whatever key name your new provider uses.

### What if I want users to have different saved cities per account?

The base build stores saved cities in localStorage, which is device-specific. To sync saved cities across devices, add Supabase Auth to the app and a saved_cities table (user_id, city_name, lat, lon). Update the save/remove logic to use Supabase queries instead of localStorage. Ask Lovable to add auth and the saved_cities table as a follow-up after the base weather app is working.

### How do I display a weather icon for each condition?

OpenWeatherMap returns an icon code like '01d' (clear sky day) or '10n' (rain night). Use it to construct an image URL: https://openweathermap.org/img/wn/{icon}@2x.png. Render it as an img tag in the weather Card. Alternatively, map icon codes to emoji (01d = ☀️, 02d = ⛅, 09d = 🌧️, 13d = ❄️, 50d = 🌫️) for a lighter, no-image approach.

### Does geolocation work on all browsers?

navigator.geolocation is supported in all modern browsers (Chrome, Firefox, Safari, Edge). However, it requires HTTPS — it does not work on HTTP pages. Lovable's published apps use HTTPS automatically. The user must also grant location permission when the browser prompts. If they deny permission, the error callback fires and you should show a message directing them to use the city search instead.

---

Source: https://www.rapidevelopers.com/how-to-build-lovable/weather-application
© RapidDev — https://www.rapidevelopers.com/how-to-build-lovable/weather-application
