Next.js starter your AI actually understands. Ship internal tools in days not weeks. Pre-order $199 $499 → [Get it now]

How to build a weather API in Node.js

Building a weather API proxy in Node.js serves two purposes: it hides your API keys from the client and adds caching to avoid hitting rate limits on the third-party weather service. As the creator of CoreUI with 25 years of backend development experience, I always proxy third-party API calls through a Node.js layer in production to maintain control over rate limiting, caching, and error handling. The key pattern is fetching from OpenWeatherMap on the server, caching responses in memory with a TTL, and returning clean, typed responses to your frontend. This also lets you normalize the response format independently of what the weather API returns.

Set up the Express server with an in-memory cache.

// src/weather/weather.router.js
import { Router } from 'express'
import { getWeather, getForecast } from './weather.service.js'

const router = Router()

router.get('/current', async (req, res, next) => {
  try {
    const { city, lat, lon } = req.query
    if (!city && (!lat || !lon)) {
      return res.status(400).json({ error: 'Provide city or lat/lon' })
    }
    const weather = await getWeather({ city, lat, lon })
    res.json(weather)
  } catch (err) {
    next(err)
  }
})

router.get('/forecast', async (req, res, next) => {
  try {
    const { city } = req.query
    if (!city) return res.status(400).json({ error: 'city is required' })
    const forecast = await getForecast(city)
    res.json(forecast)
  } catch (err) {
    next(err)
  }
})

export { router as weatherRouter }
// src/weather/weather.service.js
const cache = new Map()
const CACHE_TTL_MS = 10 * 60 * 1000 // 10 minutes
const API_KEY = process.env.OPENWEATHER_API_KEY
const BASE = 'https://api.openweathermap.org/data/2.5'

function getCached(key) {
  const entry = cache.get(key)
  if (!entry) return null
  if (Date.now() - entry.timestamp > CACHE_TTL_MS) {
    cache.delete(key)
    return null
  }
  return entry.data
}

function setCache(key, data) {
  cache.set(key, { data, timestamp: Date.now() })
}

export async function getWeather({ city, lat, lon }) {
  const cacheKey = city ?? `${lat},${lon}`
  const cached = getCached(cacheKey)
  if (cached) return cached

  const params = city
    ? `q=${encodeURIComponent(city)}`
    : `lat=${lat}&lon=${lon}`

  const res = await fetch(`${BASE}/weather?${params}&units=metric&appid=${API_KEY}`)

  if (!res.ok) {
    const err = await res.json()
    throw Object.assign(new Error(err.message), { status: res.status })
  }

  const raw = await res.json()
  const data = {
    city: raw.name,
    country: raw.sys.country,
    temperature: Math.round(raw.main.temp),
    feelsLike: Math.round(raw.main.feels_like),
    humidity: raw.main.humidity,
    description: raw.weather[0].description,
    icon: raw.weather[0].icon,
    windSpeed: Math.round(raw.wind.speed),
    updatedAt: new Date().toISOString()
  }

  setCache(cacheKey, data)
  return data
}

export async function getForecast(city) {
  const cacheKey = `forecast:${city}`
  const cached = getCached(cacheKey)
  if (cached) return cached

  const res = await fetch(
    `${BASE}/forecast?q=${encodeURIComponent(city)}&units=metric&cnt=40&appid=${API_KEY}`
  )

  if (!res.ok) throw new Error('Failed to fetch forecast')

  const raw = await res.json()
  const data = raw.list.map(item => ({
    time: item.dt_txt,
    temperature: Math.round(item.main.temp),
    description: item.weather[0].description,
    icon: item.weather[0].icon
  }))

  setCache(cacheKey, data)
  return data
}

The 10-minute cache prevents hammering the OpenWeatherMap API. Each cache entry stores the normalized data and a timestamp. Expired entries are lazily evicted on the next access. Normalizing the response shape means your frontend doesn’t break if OpenWeatherMap changes their response format.

Registering the Route

// src/app.js
import express from 'express'
import { weatherRouter } from './weather/weather.router.js'

const app = express()
app.use(express.json())
app.use('/weather', weatherRouter)

export { app }

Best Practice Note

This is the same proxy-with-cache pattern used in CoreUI dashboard backends that aggregate third-party data. For production, replace the in-memory Map cache with Redis to share cache state across multiple Node.js instances and survive restarts. Add rate limiting per IP with express-rate-limit to prevent abuse. For the React frontend that consumes this API, see how to build a weather app in React.


Speed up your responsive apps and websites with fully-featured, ready-to-use open-source admin panel templates—free to use and built for efficiency.


About the Author