Mock a weather API โ€” build your weather app while your OpenWeatherMap key activates

The weather app is the canonical first API project, and it starts the same way for everyone: sign up at OpenWeatherMap, copy the API key, make the first request โ€” and get 401 Unauthorized. The key isn't broken. New OpenWeatherMap keys can take up to 2 hours to activate, and until then every call 401s. If you're mid-tutorial, mid-workshop, or mid-livestream, you're stuck staring at an error that will fix itself sometime this afternoon.

This guide gives you an OpenWeatherMap-shaped mock with the same path and the same query params โ€” so your code doesn't change beyond the host. Build the whole app against the mock now; when the real key wakes up, swap the base URL back.

To be fair to OpenWeatherMap: the standard free plan is genuinely generous โ€” 60 calls/min, 1,000,000 calls/month, no credit card (the newer One Call API 3.0 does require a card on file even for its free 1,000 calls/day). And if what you want is real weather data with no key at all, Open-Meteo is excellent for non-commercial use. A mock is for the other cases: the activation gap, CI runs, deterministic tests ("assert the UI shows 18ยฐ"), demos that must not depend on today's actual sky, and drilling the error paths a real API only serves you by accident.

1. Get an OpenWeatherMap-shaped endpoint (30 seconds, one paste)

BASE=https://mockbird.mockbird.workers.dev
P=$(curl -s -X POST $BASE/api/projects -H 'content-type: application/json' \
     -d '{"name":"weather-mock","blank":true}')
PID=$(echo $P | sed 's/.*"id": *"\([^"]*\)".*/\1/')
KEY=$(echo $P | sed 's/.*"adminKey": *"\([^"]*\)".*/\1/')

# current weather โ€” same path OpenWeatherMap uses
curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' -d '{
  "method":"GET","path":"/data/2.5/weather",
  "body":"{\"coord\":{\"lon\":-0.13,\"lat\":51.51},\"weather\":[{\"id\":800,\"main\":\"Clear\",\"description\":\"clear sky\",\"icon\":\"01d\"}],\"base\":\"stations\",\"main\":{\"temp\":18.4,\"feels_like\":17.9,\"temp_min\":16.7,\"temp_max\":19.8,\"pressure\":1015,\"humidity\":58},\"visibility\":10000,\"wind\":{\"speed\":3.6,\"deg\":240},\"clouds\":{\"all\":0},\"dt\":{{ts}},\"sys\":{\"country\":\"GB\",\"sunrise\":1756350000,\"sunset\":1756399000},\"timezone\":3600,\"id\":2643743,\"name\":\"{{query.q}}\",\"cod\":200}"
}'

Now call it exactly like you'd call OpenWeatherMap โ€” same path, same params (appid is accepted and ignored, so your existing request code is untouched):

curl "$BASE/m/$PID/data/2.5/weather?q=London&appid=anything&units=metric"

The response has the full OpenWeatherMap current-weather shape โ€” weather[0].main, main.temp, wind, sys.country โ€” and two live details: dt is the real request timestamp ({{ts}}) and name echoes whatever city you asked for ({{query.q}}), so ?q=Tokyo answers "name":"Tokyo". Your "display the city name" code path works without a lookup table.

2. Point your app at it โ€” a host swap, nothing else

// before (real OpenWeatherMap)
const BASE = "https://api.openweathermap.org";
// during development / tests (mock)
const BASE = "https://mockbird.mockbird.workers.dev/m/YOUR_PROJECT_ID";

const r = await fetch(`${BASE}/data/2.5/weather?q=${city}&appid=${KEY}&units=metric`);
const data = await r.json();
console.log(data.name, data.main.temp, data.weather[0].description);

CORS is on by default, so this works from localhost, CodePen, or a deployed preview with no proxy. When your real key activates, change BASE back and nothing else moves.

3. The 5-day forecast, same trick

curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' -d '{
  "method":"GET","path":"/data/2.5/forecast",
  "body":"{\"cod\":\"200\",\"message\":0,\"cnt\":3,\"list\":[{\"dt\":{{ts}},\"main\":{\"temp\":18.4,\"humidity\":58},\"weather\":[{\"main\":\"Clear\",\"description\":\"clear sky\",\"icon\":\"01d\"}],\"dt_txt\":\"2026-08-28 15:00:00\"},{\"dt\":{{ts}},\"main\":{\"temp\":16.9,\"humidity\":64},\"weather\":[{\"main\":\"Clouds\",\"description\":\"scattered clouds\",\"icon\":\"03d\"}],\"dt_txt\":\"2026-08-28 18:00:00\"},{\"dt\":{{ts}},\"main\":{\"temp\":14.2,\"humidity\":71},\"weather\":[{\"main\":\"Rain\",\"description\":\"light rain\",\"icon\":\"10n\"}],\"dt_txt\":\"2026-08-28 21:00:00\"}],\"city\":{\"id\":2643743,\"name\":\"{{query.q}}\",\"country\":\"GB\"}}"
}'

curl "$BASE/m/$PID/data/2.5/forecast?q=Berlin&appid=x"   # โ†’ 3-hourly list: Clear โ†’ Clouds โ†’ Rain

Three entries covering the three icon states most weather UIs branch on (clear / clouds / rain) โ€” extend the list to 40 entries if your UI paginates a real 5-day response. 16 KB per route template is plenty.

4. Drill the error paths a real weather API makes hard to test

This is the part you can't do against the real API on purpose. Append a query param:

# the "key not activated yet" experience โ€” on demand, for your error UI
curl -i "$BASE/m/$PID/data/2.5/weather?q=London&mock_status=401"

# rate-limited (what going over 60 calls/min looks like)
curl -i "$BASE/m/$PID/data/2.5/weather?q=London&mock_status=429"

# slow response โ€” does your app show a spinner or freeze?
curl "$BASE/m/$PID/data/2.5/weather?q=London&mock_delay=2000"

# flaky network: ~half of requests fail with a random 5xx/429
curl "$BASE/m/$PID/data/2.5/weather?q=London&mock_chaos=0.5"

mock_status returns a generic error body. If your code asserts on OpenWeatherMap's exact 401 payload, make a dedicated route that always fails with it:

curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' -d '{
  "method":"GET","path":"/broken/weather","status":401,
  "body":"{\"cod\":401,\"message\":\"Invalid API key. Please see https://openweathermap.org/faq#error401 for more info.\"}"
}'

curl -i "$BASE/m/$PID/broken/weather?q=London"   # โ†’ 401 + the real OWM error shape

Same pattern works for the {"cod":"404","message":"city not found"} case. More recipes in testing loading & error states.

5. Want varied or editable data instead of fixed payloads?

Custom routes return what you wrote (plus per-request template fields โ€” {{query.q}}, {{ts}}, {{now}}, {{rand}}, full list). If you'd rather have weather records you can list, filter, edit and POST to โ€” say a cities resource with city, latitude, longitude and a condition enum โ€” create the project with resources instead of routes: that gives you full CRUD, filtering and GraphQL over the same data. See the REST quickstart; the two styles compose in one project.

Honest comparison

OpenWeatherMap (free)Open-MeteoMockbird
Real weather dataโœ”โœ”โœ— โ€” values you define
API key requiredโœ” (+ up to 2h activation)โœ—โœ— (no signup either)
Deterministic responses for testsโœ— โ€” it's live weatherโœ—โœ”
Trigger 401/429/5xx/slow on demandโœ—โœ—โœ” query params
OpenWeatherMap response shapeโ€”โœ— different schemaโœ” whatever you paste (this guide pastes OWM's)
Credit card for anythingOne Call 3.0, even free tierโœ— (non-commercial)โœ—

Rule of thumb: production weather app โ†’ OpenWeatherMap or Open-Meteo. Building, testing, teaching, or waiting out key activation โ†’ mock it, then swap the host.

Notes & limits

Create yours

The one-paste block in section 1 is the whole setup โ€” or start in the dashboard and add routes in the UI. The same project can also mock any other third-party API your app calls, serve a full CRUD REST API or a real 250-country dataset, and receive webhooks.