package main import ( "database/sql" "encoding/json" "fmt" "log" "net/http" "os" "time" _ "github.com/lib/pq" ) type WeatherResponse struct { Name string `json:"name"` Main struct { Temp float64 `json:"temp"` FeelsLike float64 `json:"feels_like"` Humidity int `json:"humidity"` } Weather []struct { Description string `json:"description"` } `json:"weather"` } type Reading struct { ID int `json:"id"` City string `json:"city"` Temp float64 `json:"temp"` FeelsLike float64 `json:"feels_like"` Humidity int `json:"humidity"` Conditions string `json:"conditions"` FetchedAt time.Time `json:"fetched_at"` } func getEnv(key string) string { val := os.Getenv(key) if val == "" { log.Fatalf("missing required env var: %s", key) } return val } func connectDB() *sql.DB { connStr := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", getEnv("DB_HOST"), getEnv("DB_PORT"), getEnv("DB_USER"), getEnv("DB_PASSWORD"), getEnv("DB_NAME"), ) db, err := sql.Open("postgres", connStr) if err != nil { log.Fatalf("failed to connect to db: %v", err) } err = db.Ping() if err != nil { log.Fatalf("failed to connect to db: %v", err) } fmt.Println("connected to PostgreSQL") return db } func createTable(db *sql.DB) { _, err := db.Exec(` CREATE TABLE IF NOT EXISTS weather_readings ( id SERIAL PRIMARY KEY, city TEXT NOT NULL, temp DOUBLE PRECISION NOT NULL, feels_like DOUBLE PRECISION NOT NULL, humidity INTEGER NOT NULL, conditions TEXT NOT NULL, fetched_at TIMESTAMP WITH TIME ZONE NOT NULL ) `) if err != nil { log.Fatalf("failed to create table: %v", err) } fmt.Println("weather_readings table ready") } func saveReading(db *sql.DB, weather WeatherResponse) (int, error) { conditions := "" if len(weather.Weather) > 0 { conditions = weather.Weather[0].Description } var id int err := db.QueryRow( `INSERT INTO weather_readings (city, temp, feels_like, humidity, conditions, fetched_at) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, weather.Name, weather.Main.Temp, weather.Main.FeelsLike, weather.Main.Humidity, conditions, time.Now(), ).Scan(&id) if err != nil { return 0, fmt.Errorf("failed to save reading: %w", err) } return id, nil } func getReadings(db *sql.DB, city string, limit int) ([]Reading, error) { rows, err := db.Query( `SELECT id, city, temp, feels_like, humidity, conditions, fetched_at FROM weather_readings WHERE city ILIKE $1 ORDER BY fetched_at DESC LIMIT $2`, city, limit, ) if err != nil { return nil, fmt.Errorf("Query failed: %w", err) } defer rows.Close() var readings []Reading for rows.Next() { var r Reading err := rows.Scan(&r.ID, &r.City, &r.Temp, &r.FeelsLike, &r.Humidity, &r.Conditions, &r.FetchedAt) if err != nil { return nil, fmt.Errorf("scan failed: %w", err) } readings = append(readings, r) } return readings, nil } func fetchWeather(apiKey, city string) (WeatherResponse, error) { url := fmt.Sprintf( "https://api.openweathermap.org/data/2.5/weather?q=%s&appid=%s&units=imperial", city, apiKey, ) resp, err := http.Get(url) if err != nil { return WeatherResponse{}, fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return WeatherResponse{}, fmt.Errorf("API returned status %d", resp.StatusCode) } var weather WeatherResponse err = json.NewDecoder(resp.Body).Decode(&weather) if err != nil { return WeatherResponse{}, fmt.Errorf("failed to parse response: %w", err) } return weather, nil } func main() { apiKey := getEnv("OPENWEATHERMAP_API_KEY") defaultCity := getEnv("WEATHER_CITY") db := connectDB() defer db.Close() createTable(db) // POST /weather/fetch?city=London - fetch current weather and save it http.HandleFunc("/weather/fetch", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } city := r.URL.Query().Get("city") if city == "" { city = defaultCity } weather, err := fetchWeather(apiKey, city) if err != nil { http.Error(w, err.Error(), http.StatusBadGateway) return } // note closure id, err := saveReading(db, weather) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "message": "weather fetched and saved", "id": id, "city": weather.Name, "temp": weather.Main.Temp, }) }) // GET /weather/history?city=London&limit=10 http.HandleFunc("/weather/history", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) } city := r.URL.Query().Get("city") if city == "" { city = defaultCity } // note closure readings, err := getReadings(db, city, 25) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(readings) }) port := ":8080" fmt.Printf("server listening on %s\n", port) log.Fatal(http.ListenAndServe(port, nil)) }