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.
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.
// 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.
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.
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.
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.
| OpenWeatherMap (free) | Open-Meteo | Mockbird | |
|---|---|---|---|
| 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 anything | One 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.
appid entirely โ which also means no real key ever needs to appear in CI config or a workshop repo.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.