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.
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.
# 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"
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.
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.
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.
| Alpha Vantage (free) | Yahoo via scraper libs | Mockbird | |
|---|---|---|---|
| Real market data | โ โ real quotes, history, fundamentals | โ while it works | โ โ fake numbers you define |
| API key / signup | key required | none โ unofficial & unsupported | none |
| Requests | 25/day free | until the 429s find you | 10,000/project/day |
| Key safe in a frontend-only deploy | โ โ ships in your bundle | n/a | no key exists |
| Writes (portfolio, trades) | โ | โ | โ persist, no auth |
| Deterministic data for tests | โ โ live market | โ | โ verbatim records, snapshots |
| Trigger 429/500/slow on demand | only by accident | constantly, but not on demand | โ query params |
| Will it exist next year? | likely | breaks whenever Yahoo changes something | free 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).
query1.finance.yahoo.com/v8/finance/chart/AAPL returned 429 Edge: Too Many Requests; keyless Alpha Vantage returns the "claim your free API key" error and their premium page states the free limit as "25 API requests per day". Check their sites for current terms.ohlc collection to the db.json with {date, open, high, low, close} rows โ ?date_gte= range queries work out of the box, which is exactly what a chart component fetches./m/YOUR_ID/graphql โ same stocks, same writes.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.