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
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
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.
1Build a weather application. Create one Supabase table:23- weather_cache: id, cache_key (text, UNIQUE — format: 'weather:lat:lon' or 'weather:city_name'), response_data (jsonb), cached_at (timestamptz default now())45No user_id or RLS needed — this is a public cache accessible by anyone.67Create an index: CREATE INDEX idx_weather_cache_key ON weather_cache(cache_key).89Set up the app shell with:10- A search bar at the top using shadcn/ui Command component11- A main content area for the weather display12- A 'Use My Location' Button next to the search bar13- A saved cities section below the main content (store in localStorage for simplicity)1415The 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.
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.
1// supabase/functions/get-weather/index.ts2import { serve } from 'https://deno.land/std@0.168.0/http/server.ts'3import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'45const corsHeaders = {6 'Access-Control-Allow-Origin': '*',7 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',8 'Content-Type': 'application/json',9}1011const CACHE_MINUTES = 3012const API_KEY = Deno.env.get('OPENWEATHER_API_KEY') ?? ''1314serve(async (req: Request) => {15 if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })1617 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')2223 if (!API_KEY) return new Response(JSON.stringify({ error: 'API key not configured' }), { status: 500, headers: corsHeaders })2425 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 })2728 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()3031 if (cached) {32 return new Response(JSON.stringify({ ...cached.response_data, _cached: true }), { headers: corsHeaders })33 }3435 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 ])4041 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 }4546 const [current, forecast] = await Promise.all([currentRes.json(), forecastRes.json()])47 const responseData = { current, forecast }4849 await supabase.from('weather_cache').upsert({ cache_key: cacheKey, response_data: responseData, cached_at: new Date().toISOString() }, { onConflict: 'cache_key' })5051 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.
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.
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.
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.
1Build the weather display UI:231. City search using shadcn/ui Command component:4 - A CommandInput that calls the OpenWeatherMap geocoding API (also via Edge Function) after 300ms of no typing5 - Show city suggestions as CommandItems with city name and country6 - Selecting a city calls the get-weather Edge Function and updates the displayed weather782. Current conditions Card:9 - Large temperature display (e.g. 22°C)10 - City name and country as heading11 - Weather description (e.g. 'Partly Cloudy') with an emoji icon based on OpenWeatherMap icon code12 - Grid of metrics: Feels Like, Humidity %, Wind Speed km/h, Visibility km13 - 'Cached' Badge in corner if _cached: true in response14153. 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 temperature19 - Tooltip showing high/low and description per day20 - Small weather emoji above each data point21224. 'Use My Location' Button:23 - On click: call navigator.geolocation.getCurrentPosition()24 - Pass coords to get-weather Edge Function as lat+lon parameters25 - Show a loading spinner while fetchingExpected 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
1import { useState, useCallback } from 'react'2import { supabase } from '@/integrations/supabase/client'34export interface WeatherData {5 current: {6 name: string7 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: number12 }13 forecast: {14 list: Array<{15 dt_txt: string16 main: { temp_max: number; temp_min: number }17 weather: Array<{ description: string; icon: string }>18 }>19 }20 _cached?: boolean21}2223export function useWeather() {24 const [data, setData] = useState<WeatherData | null>(null)25 const [isLoading, setIsLoading] = useState(false)26 const [error, setError] = useState<string | null>(null)2728 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))3637 const { data: result, error: fnError } = await supabase.functions.invoke('get-weather', {38 body: null,39 headers: {},40 })4142 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 })4647 if (!res.ok) {48 const err = await res.json()49 throw new Error(err.error ?? 'Failed to fetch weather')50 }5152 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 }, [])6061 const fetchByGeolocation = useCallback(() => {62 if (!navigator.geolocation) {63 setError('Geolocation is not supported by your browser')64 return65 }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])7273 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.
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.
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.
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.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation