Mock a stock API โ€” build your finance dashboard without an API key

The stock dashboard is a portfolio-project classic โ€” ticker tape, top movers, a watchlist, a line chart โ€” and the data side of the tutorial has quietly become the hardest part, because the free stock APIs keep dying. IEX Cloud, the one a generation of tutorials was written against, shut down entirely (its site no longer even responds). Yahoo Finance's official API was discontinued years ago, and the unofficial endpoint every scraper library leans on rate-limits strangers on sight โ€” this was the first request of the day from a fresh IP:

curl "https://query1.finance.yahoo.com/v8/finance/chart/AAPL"
# โ†’ 429 Edge: Too Many Requests

curl "https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=IBM"
# โ†’ {"Error Message": "the parameter apikey is invalid or missing. Please claim
#    your free API keyโ€ฆ"}

Alpha Vantage, the usual "just get a free key" answer, allows 25 requests per day on the free tier (their own pricing page's words: "standard API usage limit (25 API requests per day)"). A dashboard showing eight tickers spends a third of that on one page load โ€” refresh twice and you're done until tomorrow. And a frontend-only deploy ships whatever key you do get inside your JavaScript bundle.

To be fair: if your app must show real market data, real data is the product โ€” pay for it or batch it. Alpha Vantage is honest about its limits and 25 requests/day is genuinely fine for a nightly job that caches results server-side. The mock is for everything before that: building and deploying the UI, classrooms and workshops (thirty students โ‰  thirty API keys), CI that asserts on known numbers, drilling the 429/500/slow states a real feed will absolutely throw at you, and write features (portfolios, trades) that no market-data API offers at all.

1. A stock API in 30 seconds (one paste, no signup)

Quotes want a fixed roster served verbatim, not random seeding โ€” so import the records directly as a db.json. Yours to edit; here's a starter eight:

BASE=https://mockbird.mockbird.workers.dev
P=$(curl -s -X POST $BASE/api/projects/import -H 'content-type: application/json' -d '{
 "name":"stock-mock","db":{"stocks":[
  {"id":1,"symbol":"AAPL","name":"Apple Inc.","price":227.16,"change":-1.24,"changePercent":-0.54,"volume":48210345,"sector":"Technology"},
  {"id":2,"symbol":"MSFT","name":"Microsoft Corp.","price":508.45,"change":3.12,"changePercent":0.62,"volume":19882310,"sector":"Technology"},
  {"id":3,"symbol":"NVDA","name":"NVIDIA Corp.","price":171.66,"change":4.51,"changePercent":2.70,"volume":301240988,"sector":"Technology"},
  {"id":4,"symbol":"AMZN","name":"Amazon.com Inc.","price":232.33,"change":-0.87,"changePercent":-0.37,"volume":31240771,"sector":"Consumer Cyclical"},
  {"id":5,"symbol":"JPM","name":"JPMorgan Chase & Co.","price":301.03,"change":1.05,"changePercent":0.35,"volume":8114520,"sector":"Financial Services"},
  {"id":6,"symbol":"XOM","name":"Exxon Mobil Corp.","price":109.24,"change":-2.31,"changePercent":-2.07,"volume":15903118,"sector":"Energy"},
  {"id":7,"symbol":"JNJ","name":"Johnson & Johnson","price":178.90,"change":0.42,"changePercent":0.24,"volume":6221054,"sector":"Healthcare"},
  {"id":8,"symbol":"TSLA","name":"Tesla Inc.","price":346.78,"change":12.40,"changePercent":3.71,"volume":88012345,"sector":"Consumer Cyclical"}
 ]}}')
PID=$(echo $P | sed 's/.*"id": *"\([^"]*\)".*/\1/')
KEY=$(echo $P | sed 's/.*"adminKey": *"\([^"]*\)".*/\1/')

curl "$BASE/m/$PID/stocks?_limit=2"

Records come back exactly as you wrote them โ€” numbers stay numbers, negatives stay negative. Add more tickers by editing the JSON or just POSTing to /stocks later.

2. Query it like a stock API

# quote lookup
curl "$BASE/m/$PID/stocks?symbol=AAPL"

# top movers
curl "$BASE/m/$PID/stocks?sortBy=changePercent&order=desc&_limit=3"

# gainers only (range operators work on any numeric field)
curl "$BASE/m/$PID/stocks?changePercent_gte=0"

# search box
curl "$BASE/m/$PID/stocks?q=apple"

# sector tab
curl "$BASE/m/$PID/stocks?sector=Energy"

# detail page
curl "$BASE/m/$PID/stocks/3"

# ticker-tape payload โ€” only the fields the strip renders
curl "$BASE/m/$PID/stocks?select=symbol,price,changePercent"

3. A ticker that moves between polls

Static records are right for tests โ€” but a dashboard demo looks better when the number changes on every refresh. One custom route gives you a templated quote endpoint whose price is re-rolled per request:

curl -s -X POST $BASE/api/projects/$PID/routes -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' \
  -d '{"method":"GET","path":"/quote/:symbol","body":"{\"symbol\":\"{{params.symbol}}\",\"price\":{{rand}},\"ts\":\"{{now}}\"}"}'

curl "$BASE/m/$PID/quote/AAPL"
# {"symbol":"AAPL","price":680528,"ts":"2026-09-08T21:01:27.026Z"}
curl "$BASE/m/$PID/quote/AAPL"
# {"symbol":"AAPL","price":846876,"ts":"2026-09-08T21:01:27.266Z"}

{{rand}} is a random integer (treat it as cents, or divide in the client); {{now}} is a live ISO timestamp โ€” handy for asserting your UI actually re-renders on poll. Point your setInterval fetch at it.

4. A portfolio that persists (the part no market-data API has)

curl -s -X POST $BASE/api/projects/$PID/resources -H "x-admin-key: $KEY" \
  -H 'content-type: application/json' \
  -d '{"name":"trades","seed":0,"fields":[{"name":"stockId","type":"refId"},{"name":"side","type":"oneOf","values":["buy","sell"]},{"name":"shares","type":"number"}]}'

# "Buy" button
curl -X POST "$BASE/m/$PID/trades" -H 'content-type: application/json' \
  -d '{"stockId":8,"side":"buy","shares":10}'

# portfolio page โ€” join the full stock record onto each trade
curl "$BASE/m/$PID/trades?_expand=stock"

The write persists across reloads, and the _expand join returns each trade with its full stock object โ€” symbol, live-edited price and all โ€” so the portfolio table is one request.

5. Drill the states a real feed will throw at you

Rate limits aren't hypothetical here โ€” 429 is the defining failure mode of free market data. Rehearse it deterministically:

# backoff-and-retry logic โ€” exactly two 429s, then success
curl -i "$BASE/m/$PID/stocks?mock_seq=429,429,200"

# a polling dashboard under a flaky feed โ€” ~40% of polls fail with 429
curl -i "$BASE/m/$PID/stocks?mock_chaos=0.4&mock_chaos_status=429"

# skeleton screens โ€” a real 2-second response
curl "$BASE/m/$PID/stocks?mock_delay=2000"

# error banner โ€” a real 500
curl -i "$BASE/m/$PID/stocks?mock_status=500"

More recipes in testing loading & error states and mocking rate limits.

Honest comparison

Alpha Vantage (free)Yahoo via scraper libsMockbird
Real market dataโœ” โ€” real quotes, history, fundamentalsโœ” while it worksโœ— โ€” fake numbers you define
API key / signupkey requirednone โ€” unofficial & unsupportednone
Requests25/day freeuntil the 429s find you10,000/project/day
Key safe in a frontend-only deployโœ— โ€” ships in your bundlen/ano key exists
Writes (portfolio, trades)โœ—โœ—โœ” persist, no auth
Deterministic data for testsโœ— โ€” live marketโœ—โœ” verbatim records, snapshots
Trigger 429/500/slow on demandonly by accidentconstantly, but not on demandโœ” query params
Will it exist next year?likelybreaks whenever Yahoo changes somethingfree while in beta

Rule of thumb: shipping a real finance product โ†’ pay for a market-data feed and proxy it server-side. Building the UI, teaching a class, running CI, keeping a portfolio demo alive โ†’ mock it, then swap the base URL (and label the demo numbers as sample data โ€” don't let anyone mistake them for quotes).

Notes & limits

Create yours

The one-paste block in section 1 is the whole setup โ€” or start in the dashboard and click instead. The same project can also mock a currency/exchange-rate API, a news API, a movie API, any third-party API your app calls, and hand your test suite deterministic fixtures.