Foreca Weather API · Documentation

JavaScript Weather API Quickstart

Fetch an hourly forecast with Node.js, then show it and a weather map in a web page through a small server of your own, so your key never reaches the browser.

Get an API key

Before you start

You need Node.js 18 or later, which has fetch built in, and an API key. The Quickstart shows how to get a key.

Terminal
export FORECA_API_KEY="paste-your-key-here"

In Windows PowerShell, set the key with $env:FORECA_API_KEY = "paste-your-key-here".

Node.js

Save this as forecast.mjs. The .mjs ending lets it use await at the top level.

forecast.mjs
const API = 'https://weatherapi.foreca.net'
const headers = { Authorization: `Bearer ${process.env.FORECA_API_KEY}` }

const res = await fetch(`${API}/api/v1/forecast/hourly/24.94,60.17?periods=12`, { headers })
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`)
const { forecast } = await res.json()

for (const hour of forecast) {
  console.log(hour.time.slice(11, 16), `${hour.temperature} °C`, `wind ${hour.windSpeed} m/s`)
}

Run it with node forecast.mjs. It prints the time, temperature and wind for each of the next 12 hours.

In the browser, through your own server

Never put your key in code that runs in the browser: anyone who opens the page can read it and use it on your plan. Let a small server of your own call the API instead. The page asks your server, and only the server knows the key.

The server answers /forecast by calling the API with your key, and serves the page for everything else. Its /capabilities and /tiles routes are for the weather map below:

server.mjs
import { createServer } from 'node:http'
import { readFile } from 'node:fs/promises'

const API = 'https://weatherapi.foreca.net'
const headers = { Authorization: `Bearer ${process.env.FORECA_API_KEY}` }

async function forward(res, path) {
  try {
    const api = await fetch(API + path, { headers })
    res.writeHead(api.status, {
      'Content-Type': api.headers.get('content-type') ?? 'application/octet-stream',
      'Cache-Control': 'max-age=300',
    })
    res.end(Buffer.from(await api.arrayBuffer()))
  } catch (err) {
    res.writeHead(502, { 'Content-Type': 'application/json' })
    res.end(JSON.stringify({ message: err.message }))
  }
}

createServer(async (req, res) => {
  const url = new URL(req.url, 'http://localhost')
  const tile = url.pathname.match(/^\/tiles\/(\d+)\/(\d+)\/(\d+)\/([\dTZ:-]+)\/(\d+)$/)
  if (url.pathname === '/forecast') {
    const location = url.searchParams.get('location') ?? '24.94,60.17'
    return forward(res, `/api/v1/forecast/hourly/${encodeURIComponent(location)}?periods=12`)
  }
  if (url.pathname === '/capabilities') return forward(res, '/api/v1/capabilities')
  if (tile) return forward(res, `/api/v1/image/tile/${tile.slice(1).join('/')}`)
  const page = url.pathname === '/map' ? './map.html' : './index.html'
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
  res.end(await readFile(new URL(page, import.meta.url)))
}).listen(8080, () => console.log('Open http://localhost:8080'))

The page sits next to it, fetches /forecast and lists the hours:

index.html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Hourly forecast</title>
<h1>Helsinki, next 12 hours</h1>
<ul id="forecast"></ul>
<script type="module">
  const list = document.getElementById('forecast')
  const res = await fetch('/forecast?location=24.94,60.17')
  if (!res.ok) {
    list.textContent = await res.text()
  } else {
    const { forecast } = await res.json()
    for (const hour of forecast) {
      const item = document.createElement('li')
      item.textContent = `${hour.time.slice(11, 16)}  ${hour.temperature} °C, wind ${hour.windSpeed} m/s`
      list.append(item)
    }
  }
</script>

Start it with node server.mjs and open http://localhost:8080. In production, let the server forward only the requests your page needs, and cache the answers for up to 15 minutes: in Europe and the continental USA the first hours of a forecast are updated about that often, the rest a few times a day.

A weather map in the browser

Map tiles need the key too, and a map library fetches them from the user's device. So the same server forwards two more requests: /capabilities, which lists each layer's available times, and /tiles/…, which returns one tile. It forwards only tile requests in the shape the map asks for, so the server cannot be used to call the rest of the API on your key.

Save this next to the server as map.html. It draws an OpenStreetMap base map with Leaflet and lays the temperature layer over it at the current time:

map.html
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Weather map</title>
<link rel="stylesheet" href="https://unpkg.com/[email protected]/dist/leaflet.css">
<script src="https://unpkg.com/[email protected]/dist/leaflet.js"></script>
<div id="map" style="height: 100vh"></div>
<script type="module">
  const LAYER = 2
  const map = L.map('map').setView([60.17, 24.94], 5)
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    attribution: '© OpenStreetMap contributors',
  }).addTo(map)

  const { images } = await (await fetch('/capabilities')).json()
  const layer = images.find((image) => image.id === LAYER)
  const time = layer.times.available[layer.times.current]
  L.tileLayer(`/tiles/{z}/{x}/{y}/${time}/${LAYER}`, {
    opacity: 0.7,
    attribution: ['Foreca', layer.attribution].filter(Boolean).join(' · '),
  }).addTo(map)
</script>

Open http://localhost:8080/map. Each tile counts as one request, so the server lets the browser keep tiles for five minutes. Change LAYER to show radar, satellite or another layer: Weather Maps lists every layer id with its coverage. Show the attribution the map draws in the corner; it names the data source when a layer has one.

Errors

Both examples pass an error status on with the API's JSON body, for example 401: {"error":"invalid_token",…}. When a request fails lists what each error means. On 429, wait the number of seconds in the Retry-After header before you retry.

Next steps