Skip to main content
RapidDev - Software Development Agency

How to Build a Weather Application with Lovable

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.

What you'll build

  • City search using shadcn/ui Command component with debounced autocomplete
  • Current weather Card showing temperature, feels-like, humidity, wind speed, and weather icon
  • 7-day forecast rendered as a Recharts AreaChart or BarChart
  • Geolocation support using the browser's navigator.geolocation API
  • Supabase Edge Function weather proxy that keeps the API key server-side
  • Response caching in a Supabase table to avoid repeated API calls for the same city
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner13 min read1–1.5 hoursLovable Free or higherApril 2026RapidDev Engineering Team
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.

What you're building

A weather app that uses a third-party API needs to keep its API key hidden from the browser. The approach used in this guide is a Supabase Edge Function that acts as a proxy: the frontend calls your Edge Function, which calls the weather API with the key stored in Deno.env, and returns the result. The API key is never sent to the browser.

Caching prevents burning through free tier limits. The weather_cache table stores the API response JSON for each city and lat/lng combination. When the Edge Function receives a request, it first checks if a cache row exists with cached_at within the last 30 minutes. If so, it returns the cached data without calling the weather API. If not, it calls the API, stores the result, and returns it.

The city search uses shadcn/ui's Command component. As the user types, a debounced query calls the geocoding API (also via Edge Function) to return matching city names. Selecting a city fetches the weather for those coordinates. The geolocation button calls navigator.geolocation.getCurrentPosition() and passes the coordinates directly to the weather Edge Function.

Final result

A weather app where the API key is secure, searches are fast, and repeated lookups for the same city use cached data.

Tech stack

LovableFull-stack app generation
Supabase Edge FunctionsWeather API proxy (Deno)
SupabaseResponse cache storage
OpenWeatherMap APIWeather data source (free tier)
shadcn/uiCommand, Card, Badge
Recharts7-day forecast chart

Prerequisites

  • 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

Build steps

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.

prompt.txt
1Build a weather application. Create one Supabase table:
2
3- weather_cache: id, cache_key (text, UNIQUE format: 'weather:lat:lon' or 'weather:city_name'), response_data (jsonb), cached_at (timestamptz default now())
4
5No user_id or RLS needed this is a public cache accessible by anyone.
6
7Create an index: CREATE INDEX idx_weather_cache_key ON weather_cache(cache_key).
8
9Set up the app shell with:
10- A search bar at the top using shadcn/ui Command component
11- A main content area for the weather display
12- A 'Use My Location' Button next to the search bar
13- A saved cities section below the main content (store in localStorage for simplicity)
14
15The 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
1// supabase/functions/get-weather/index.ts
2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'
3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'
4
5const corsHeaders = {
6 'Access-Control-Allow-Origin': '*',
7 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
8 'Content-Type': 'application/json',
9}
10
11const CACHE_MINUTES = 30
12const API_KEY = Deno.env.get('OPENWEATHER_API_KEY') ?? ''
13
14serve(async (req: Request) => {
15 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })
16
17 const supabase = createClient(Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '')
18 const url = new URL(req.url)
19 const lat = url.searchParams.get('lat')
20 const lon = url.searchParams.get('lon')
21 const city = url.searchParams.get('city')
22
23 if (!API_KEY) return new Response(JSON.stringify({ error: 'API key not configured' }), { status: 500, headers: corsHeaders })
24
25 const cacheKey = lat && lon ? `weather:${parseFloat(lat).toFixed(2)}:${parseFloat(lon).toFixed(2)}` : `weather:${city?.toLowerCase().trim()}`
26 if (!cacheKey.includes(':')) return new Response(JSON.stringify({ error: 'Provide lat+lon or city' }), { status: 400, headers: corsHeaders })
27
28 const staleAfter = new Date(Date.now() - CACHE_MINUTES * 60 * 1000).toISOString()
29 const { data: cached } = await supabase.from('weather_cache').select('response_data, cached_at').eq('cache_key', cacheKey).gte('cached_at', staleAfter).single()
30
31 if (cached) {
32 return new Response(JSON.stringify({ ...cached.response_data, _cached: true }), { headers: corsHeaders })
33 }
34
35 const query = lat && lon ? `lat=${lat}&lon=${lon}` : `q=${encodeURIComponent(city ?? '')}`
36 const [currentRes, forecastRes] = await Promise.all([
37 fetch(`https://api.openweathermap.org/data/2.5/weather?${query}&appid=${API_KEY}&units=metric`),
38 fetch(`https://api.openweathermap.org/data/2.5/forecast?${query}&appid=${API_KEY}&units=metric`),
39 ])
40
41 if (!currentRes.ok) {
42 const err = await currentRes.json()
43 return new Response(JSON.stringify({ error: err.message ?? 'City not found' }), { status: 404, headers: corsHeaders })
44 }
45
46 const [current, forecast] = await Promise.all([currentRes.json(), forecastRes.json()])
47 const responseData = { current, forecast }
48
49 await supabase.from('weather_cache').upsert({ cache_key: cacheKey, response_data: responseData, cached_at: new Date().toISOString() }, { onConflict: 'cache_key' })
50
51 return new Response(JSON.stringify(responseData), { headers: corsHeaders })
52})

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.

prompt.txt
1Add 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.

prompt.txt
1Build the weather display UI:
2
31. City search using shadcn/ui Command component:
4 - A CommandInput that calls the OpenWeatherMap geocoding API (also via Edge Function) after 300ms of no typing
5 - Show city suggestions as CommandItems with city name and country
6 - Selecting a city calls the get-weather Edge Function and updates the displayed weather
7
82. Current conditions Card:
9 - Large temperature display (e.g. 22°C)
10 - City name and country as heading
11 - Weather description (e.g. 'Partly Cloudy') with an emoji icon based on OpenWeatherMap icon code
12 - Grid of metrics: Feels Like, Humidity %, Wind Speed km/h, Visibility km
13 - 'Cached' Badge in corner if _cached: true in response
14
153. 7-day forecast Recharts AreaChart:
16 - Parse the forecast API response (it returns 3-hour intervals, group by day, take the noon entry)
17 - X-axis: day names (Mon, Tue, Wed...)
18 - Two lines: max temperature and min temperature
19 - Tooltip showing high/low and description per day
20 - Small weather emoji above each data point
21
224. 'Use My Location' Button:
23 - On click: call navigator.geolocation.getCurrentPosition()
24 - Pass coords to get-weather Edge Function as lat+lon parameters
25 - 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

src/hooks/useWeather.ts
1import { useState, useCallback } from 'react'
2import { supabase } from '@/integrations/supabase/client'
3
4export interface WeatherData {
5 current: {
6 name: string
7 sys: { country: string }
8 main: { temp: number; feels_like: number; humidity: number }
9 wind: { speed: number }
10 weather: Array<{ description: string; icon: string }>
11 visibility: number
12 }
13 forecast: {
14 list: Array<{
15 dt_txt: string
16 main: { temp_max: number; temp_min: number }
17 weather: Array<{ description: string; icon: string }>
18 }>
19 }
20 _cached?: boolean
21}
22
23export function useWeather() {
24 const [data, setData] = useState<WeatherData | null>(null)
25 const [isLoading, setIsLoading] = useState(false)
26 const [error, setError] = useState<string | null>(null)
27
28 const fetchWeather = useCallback(async (params: { city?: string; lat?: number; lon?: number }) => {
29 setIsLoading(true)
30 setError(null)
31 try {
32 const searchParams = new URLSearchParams()
33 if (params.city) searchParams.set('city', params.city)
34 if (params.lat !== undefined) searchParams.set('lat', String(params.lat))
35 if (params.lon !== undefined) searchParams.set('lon', String(params.lon))
36
37 const { data: result, error: fnError } = await supabase.functions.invoke('get-weather', {
38 body: null,
39 headers: {},
40 })
41
42 const url = `${(supabase as any).supabaseUrl}/functions/v1/get-weather?${searchParams}`
43 const res = await fetch(url, {
44 headers: { apikey: (supabase as any).supabaseKey },
45 })
46
47 if (!res.ok) {
48 const err = await res.json()
49 throw new Error(err.error ?? 'Failed to fetch weather')
50 }
51
52 const weatherData = await res.json()
53 setData(weatherData)
54 } catch (err) {
55 setError(err instanceof Error ? err.message : 'Unknown error')
56 } finally {
57 setIsLoading(false)
58 }
59 }, [])
60
61 const fetchByGeolocation = useCallback(() => {
62 if (!navigator.geolocation) {
63 setError('Geolocation is not supported by your browser')
64 return
65 }
66 setIsLoading(true)
67 navigator.geolocation.getCurrentPosition(
68 (pos) => fetchWeather({ lat: pos.coords.latitude, lon: pos.coords.longitude }),
69 () => { setError('Location access denied'); setIsLoading(false) }
70 )
71 }, [fetchWeather])
72
73 return { data, isLoading, error, fetchWeather, fetchByGeolocation }
74}

Customization ideas

Air quality index display

Add a call to the OpenWeatherMap Air Pollution API (free, same key) in the Edge Function. Display an AQI card below the current conditions with PM2.5, PM10, CO, and NO2 values. Color-code the AQI: green (1-2), yellow (3), orange (4), red (5). Cache air quality data separately with the same 30-minute cache approach.

Weather alerts and severe conditions

Use OpenWeatherMap's One Call API 3.0 (paid) or parse the free forecast for extreme conditions (temperature > 35°C, wind > 60 km/h, thunderstorm description). Show a red Alert banner at the top of the weather Card when severe conditions are detected in the next 24 hours.

Historical weather comparison

Store weather_cache entries indefinitely (remove the 30-minute cache expiry filter for historical queries). Add a 'This time last year' Card that fetches the cached data from exactly one year ago for the same city. If no cache exists from that date, show 'No historical data available.'

Radar map integration

Embed a weather radar map using OpenWeatherMap's tile layer (free). Use a Leaflet.js map component with the precipitation, clouds, or temperature overlay layer. Ask Lovable to add a map Tab below the forecast chart showing a centered map tile at the searched city's coordinates.

Common pitfalls

Pitfall: Calling the weather API directly from the frontend

How to avoid: 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.

Pitfall: Using VITE_OPENWEATHER_API_KEY on the frontend

How to avoid: 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.

Pitfall: Not handling the case where geolocation is denied

How to avoid: 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.

Pitfall: Not grouping the 3-hour forecast intervals into daily summaries

How to avoid: 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.

AI prompts to try

Copy these prompts to build this project faster.

ChatGPT Prompt

I'm parsing OpenWeatherMap's /forecast API response in TypeScript. The response has a list array with 3-hour interval entries. Help me write a function parseDailyForecast(list: ForecastItem[]) that groups entries by calendar date, finds the entry closest to noon for each day, and extracts: date (string), tempMax (number), tempMin (number, minimum across all intervals that day), description (string), icon (string). Return an array of DailyForecast objects for the next 7 days.

Lovable Prompt

Add a 'Weather this week' summary section below the forecast chart. Show 7 Cards in a horizontal scroll row, one per day. Each Card shows: day of week (Mon, Tue...), a weather emoji based on the icon code, high temp, low temp, and precipitation probability percentage. Cards for today and tomorrow have a blue highlight border. Clicking a day Card scrolls to that day's detail in the chart tooltip.

Build Prompt

In Supabase, create a scheduled Edge Function that runs every hour and deletes weather_cache rows where cached_at is more than 24 hours old. This prevents the cache table from growing indefinitely. Use DELETE FROM weather_cache WHERE cached_at < now() - interval '24 hours'. Log how many rows were deleted as a JSON response. Schedule it with pg_cron: SELECT cron.schedule('clean-weather-cache', '0 * * * *', ...).

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.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help building your app?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.