Mock a news API โ€” fix the NewsAPI 426 error on deploy day

The 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.

To be fair to NewsAPI: the localhost-only rule is a defensible response to a real problem โ€” an API key shipped in browser JavaScript is public, and scraped keys were presumably burning their free tier. Their docs are clean and the free plan is honestly labeled a development tier. If you want real headlines in a deployed app, the supported answer is to keep the key server-side behind a tiny proxy route of your own (or pick a provider whose free tier allows browser calls). A mock is for the other cases: building and deploying the frontend, portfolio demos that must not die when a reviewer clicks the link, CI, tests that assert on known article data, and drilling the error paths a real API only serves you by accident.

1. Get a NewsAPI-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":"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.

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

// 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.)

3. The search endpoint, 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":"/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.

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

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.

5. Want editable articles instead of fixed payloads?

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.

Honest comparison

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 keyrequirednone (no signup either)
Requests100/day10,000/project/day
Deterministic data for testsโœ— โ€” it's the newsโœ”
Trigger 401/426/429/slow on demandโœ—โœ” query params
Commercial useforbidden on free tierfree 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).

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 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.