426 error on deploy dayThe news app is a canonical first API project, and it has a famous failure mode: everything works on localhost, you deploy to Netlify or Vercel, and every request suddenly fails with 426 Upgrade Required. Nothing in your code changed. NewsAPI's free Developer plan blocks browser requests from any origin except localhost โ the moment your app lives on a real domain, the free tier stops answering it. Add the other Developer-plan limits (100 requests/day, articles delayed 24 hours, no commercial use) and the fact that the first paid tier starts at $449/month, and a lot of tutorial-followers and portfolio-builders hit a wall the tutorial never mentioned.
This guide gives you a NewsAPI-shaped mock with the same paths and the same JSON shape โ /v2/top-headlines and /v2/everything, articles[].source.name, publishedAt, all of it โ served with CORS open to every origin. Your code changes by one base URL.
BASE=https://mockbird.mockbird.workers.dev
P=$(curl -s -X POST $BASE/api/projects -H 'content-type: application/json' \
-d '{"name":"news-mock","blank":true}')
PID=$(echo $P | sed 's/.*"id": *"\([^"]*\)".*/\1/')
KEY=$(echo $P | sed 's/.*"adminKey": *"\([^"]*\)".*/\1/')
# top headlines โ same path NewsAPI uses
curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"method":"GET","path":"/v2/top-headlines",
"body":"{\"status\":\"ok\",\"totalResults\":2,\"articles\":[{\"source\":{\"id\":\"mockbird\",\"name\":\"Mockbird Wire\"},\"author\":\"Ada Byline\",\"title\":\"Top headlines for {{query.country}} load from any origin\",\"description\":\"No 426 here: this endpoint answers deployed apps, not just localhost.\",\"url\":\"https://example.com/story-1\",\"urlToImage\":\"https://mockbird.mockbird.workers.dev/m/demo/img/640x360\",\"publishedAt\":\"{{now}}\",\"content\":\"No 426 here: this endpoint answers deployed apps, not just localhost. [+1234 chars]\"},{\"source\":{\"id\":\"mockbird\",\"name\":\"Mockbird Wire\"},\"author\":\"Ada Byline\",\"title\":\"publishedAt is live and regenerated per request\",\"description\":\"The {{now}} template placeholder stamps the real request time.\",\"url\":\"https://example.com/story-2\",\"urlToImage\":\"https://mockbird.mockbird.workers.dev/m/demo/img/640x360\",\"publishedAt\":\"{{now}}\",\"content\":\"The template placeholder stamps the real request time. [+987 chars]\"}]}"
}'
Now call it exactly like you'd call NewsAPI โ same path, same params (apiKey is accepted and ignored, so your existing request code is untouched):
curl "$BASE/m/$PID/v2/top-headlines?country=us&apiKey=anything"
The response has the full NewsAPI top-headlines shape โ status, totalResults, articles[].source.name, author, title, urlToImage, content โ with two live details: publishedAt is the real request time ({{now}}) and the first title echoes whatever ?country= you asked for ({{query.country}}). urlToImage points at Mockbird's built-in placeholder image endpoint, so your <img> tags render actual pictures instead of broken icons.
// before (real NewsAPI โ works on localhost, 426s once deployed on the free plan)
const BASE = "https://newsapi.org";
// during development / on your deployed preview (mock)
const BASE = "https://mockbird.mockbird.workers.dev/m/YOUR_PROJECT_ID";
const r = await fetch(`${BASE}/v2/top-headlines?country=us&apiKey=${KEY}`);
const { articles } = await r.json();
articles.forEach(a => console.log(a.source.name, "โ", a.title, a.publishedAt));
CORS is on by default and open to every origin โ localhost, CodePen, your Netlify deploy, a classmate's machine. The exact thing the 426 takes away is the thing this gives back. (Bonus: no real API key ever appears in your repo or bundle.)
curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"method":"GET","path":"/v2/everything",
"body":"{\"status\":\"ok\",\"totalResults\":1,\"articles\":[{\"source\":{\"id\":\"mockbird\",\"name\":\"Mockbird Wire\"},\"author\":\"Ada Byline\",\"title\":\"Search results for {{query.q}}\",\"description\":\"The q param is echoed straight back โ proof the mock is alive.\",\"url\":\"https://example.com/search-1\",\"urlToImage\":\"https://mockbird.mockbird.workers.dev/m/demo/img/640x360\",\"publishedAt\":\"{{now}}\",\"content\":\"The q param is echoed straight back. [+456 chars]\"}]}"
}'
curl "$BASE/m/$PID/v2/everything?q=bitcoin&sortBy=publishedAt&apiKey=x" # โ title: "Search results for bitcoin"
Your search box now demonstrably drives the response โ ?q=bitcoin answers "Search results for bitcoin" โ without a lookup table or a key.
Append a query param to any of the routes above:
# what the free plan does to your deployed app โ on demand, for your error UI
curl -i "$BASE/m/$PID/v2/top-headlines?country=us&mock_status=426"
# invalid key / rate limited
curl -i "$BASE/m/$PID/v2/top-headlines?country=us&mock_status=401"
curl -i "$BASE/m/$PID/v2/top-headlines?country=us&mock_status=429"
# slow response โ skeleton screens or a frozen page?
curl "$BASE/m/$PID/v2/top-headlines?country=us&mock_delay=2000"
# flaky network: ~half of requests fail with a random 5xx/429
curl "$BASE/m/$PID/v2/top-headlines?country=us&mock_chaos=0.5"
mock_status returns a generic error body. NewsAPI's real errors have their own shape (status/code/message) โ if your code branches on it, make a dedicated route that always fails with the exact payload:
curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"method":"GET","path":"/broken/headlines","status":426,
"body":"{\"status\":\"error\",\"code\":\"corsNotAllowed\",\"message\":\"Requests from the browser are not allowed on the Developer plan, except from localhost.\"}"
}'
curl -i "$BASE/m/$PID/broken/headlines" # โ 426 + the real NewsAPI error shape
Same pattern works for {"status":"error","code":"apiKeyInvalid","message":"Your API key is invalid or incorrect. ..."} with status 401. More recipes in testing loading & error states.
Custom routes return what you wrote. If you'd rather have article records you can list, filter, sort, search and POST to โ infinite-scroll demos, "save article" features, an admin screen โ add an articles resource to the same project:
curl -s -X POST $BASE/api/projects/$PID/resources -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"name":"articles",
"fields":[{"name":"title","type":"sentence"},{"name":"author","type":"fullName"},
{"name":"description","type":"paragraph"},{"name":"url","type":"url"},
{"name":"publishedAt","type":"date"},
{"name":"category","type":"oneOf","values":["business","technology","sports","science"]}]
}'
# 20 seeded articles with full query tools:
curl "$BASE/m/$PID/articles?category=technology&_sort=publishedAt&_order=desc&_limit=5"
curl "$BASE/m/$PID/articles?q=cloud&select=title,author"
# POST your own headline โ it persists, and shows up in every later GET
curl -X POST "$BASE/m/$PID/articles" -H 'content-type: application/json' \
-d '{"title":"My own breaking headline","author":"Me","category":"technology"}'
The two styles compose in one project: NewsAPI-shaped routes for the drop-in story, a real CRUD resource (with GraphQL, pagination and snapshots) for everything stateful.
| NewsAPI (free Developer) | Mockbird | |
|---|---|---|
| Real headlines | โ (delayed 24 hours) | โ โ articles you define |
| Works from a deployed browser app | โ โ localhost only, otherwise 426 | โ CORS open to any origin |
| API key | required | none (no signup either) |
| Requests | 100/day | 10,000/project/day |
| Deterministic data for tests | โ โ it's the news | โ |
| Trigger 401/426/429/slow on demand | โ | โ query params |
| Commercial use | forbidden on free tier | free while in beta |
| First paid tier | $449/month | โ |
Rule of thumb: production news product โ a paid plan behind your own server-side proxy, or a provider whose free tier allows browser calls. Building, testing, teaching, or keeping a portfolio deploy alive โ mock it, then swap the host (and label the demo data as sample content).
apiKey entirely โ which also means no real key ever needs to appear in CI config, a workshop repo, or your deployed bundle.401 apiKeyMissing/apiKeyInvalid; the free Developer plan is 100 requests/day with a 24-hour article delay, browser requests blocked outside localhost (the 426 / corsNotAllowed behavior, in place since 2020), commercial use not permitted; the Business tier is $449/month. Check their pricing page for current numbers.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 a movie API, a weather API, host a real countries dataset, any other third-party API your app calls, serve a full CRUD REST API, and receive webhooks.