The movie app is probably the single most-built portfolio project there is โ every React, Vue and Flutter course has one โ and step one is always the same: go sign up for a TMDB API key. Keyless requests get you nowhere on either of the two APIs the tutorials use:
curl https://api.themoviedb.org/3/movie/550
# โ 401 {"status_code":7,"status_message":"Invalid API key: You must be granted a valid key."}
curl "https://www.omdbapi.com/?t=inception"
# โ 401 {"Response":"False","Error":"No API key provided."}
Signup is a small tax once โ but it compounds: a classroom of thirty students is thirty signups; a frontend-only deploy means your key ships in the JavaScript bundle where anyone can lift it; CI needs the key as a secret; and neither API will serve you a 500 or a slow response on demand when you want to build decent loading and error states. While you're building the UI, none of that buys you anything.
BASE=https://mockbird.mockbird.workers.dev
P=$(curl -s -X POST $BASE/api/projects -H 'content-type: application/json' \
-d '{"name":"movie-mock","blank":true}')
PID=$(echo $P | sed 's/.*"id": *"\([^"]*\)".*/\1/')
KEY=$(echo $P | sed 's/.*"adminKey": *"\([^"]*\)".*/\1/')
curl -s -X POST $BASE/api/projects/$PID/resources -H "x-admin-key: $KEY" \
-H 'content-type: application/json' -d '{
"name":"movies","seed":25,
"fields":[{"name":"title","type":"title"},
{"name":"year","type":"oneOf","values":[1972,1994,1999,2008,2014,2019,2023]},
{"name":"genre","type":"oneOf","values":["action","comedy","drama","sci-fi","horror","romance"]},
{"name":"rating","type":"rating"},
{"name":"poster","type":"image"},
{"name":"overview","type":"paragraph"},
{"name":"director","type":"fullName"}]
}'
curl "$BASE/m/$PID/movies?_limit=1"
That returns 25 seeded movies shaped like this โ note the poster is a URL that actually renders (more below):
{
"id": 1,
"title": "Eye Moment Power Cloud Hour",
"year": 1994,
"genre": "comedy",
"rating": 1.5,
"poster": "https://mockbird.mockbird.workers.dev/m/YOUR_ID/img/640x480?seed=1511",
"overview": "Music month road problem case child window. โฆ",
"director": "Olivia Martinez"
}
oneOf picks verbatim from the values you list (the years above come back as real integers), rating is a 1โ5 float, and every field type is swappable โ see field types.
Everything a movie-browser UI needs is a query parameter โ no extra endpoints to define:
# search box (TMDB: /search/movie?query=โฆ)
curl "$BASE/m/$PID/movies?q=cloud"
# genre tab (TMDB: /discover/movie?with_genres=โฆ)
curl "$BASE/m/$PID/movies?genre=sci-fi"
# "Top rated" (TMDB: sort_by=vote_average.desc)
curl "$BASE/m/$PID/movies?sortBy=rating&order=desc&_limit=5"
# minimum-rating slider (TMDB: vote_average.gte)
curl "$BASE/m/$PID/movies?rating_gte=4"
# pagination for the grid โ X-Total-Count header included
curl -i "$BASE/m/$PID/movies?_page=2&_limit=10"
# detail page
curl "$BASE/m/$PID/movies/1"
# lighter list payloads for the grid
curl "$BASE/m/$PID/movies?select=title,rating,poster"
The image field type points at Mockbird's own placeholder-image endpoint โ deterministic SVG posters served from the same host, so <img src={movie.poster}> just works with no third-party image service, no hotlink breakage, and no key. You can also compose poster URLs yourself, at poster-ish aspect ratios, with a label:
https://mockbird.mockbird.workers.dev/m/YOUR_ID/img/300x450?seed=7&text=Poster
Same seed โ same colors forever, which keeps visual-regression screenshots stable. Details in the placeholder image guide.
On TMDB, watchlist writes require user authentication. Here, writes are the point โ add a second resource and POST to it:
curl -s -X POST $BASE/api/projects/$PID/resources -H "x-admin-key: $KEY" \
-H 'content-type: application/json' \
-d '{"name":"watchlist","seed":0,"fields":[{"name":"movieId","type":"refId"},{"name":"note","type":"sentence"}]}'
# "Add to watchlist" button
curl -X POST "$BASE/m/$PID/watchlist" -H 'content-type: application/json' \
-d '{"movieId":1,"note":"must see"}'
# watchlist page โ join the full movie record onto each entry
curl "$BASE/m/$PID/watchlist?_expand=movie"
The write persists: it shows up in every later GET, survives page reloads, and the _expand join gives your watchlist page full movie objects in one request. POSTing new movies works the same way โ a "suggest a film" form is one fetch.
# skeleton screens โ a real 2-second response
curl "$BASE/m/$PID/movies?mock_delay=2000"
# error page โ a real 500
curl -i "$BASE/m/$PID/movies?mock_status=500"
# retry logic โ fails twice, then succeeds, deterministically
curl -i "$BASE/m/$PID/movies?mock_seq=500,500,200"
Recipes for every state in testing loading & error states.
| TMDB (free) | OMDb (free) | Mockbird | |
|---|---|---|---|
| Real movie data & posters | โ โ excellent | โ | โ โ seeded fake data you define |
| API key / signup | key required (non-commercial use, attribution) | key by email, 1,000 req/day | none |
| Key safe in a frontend-only deploy | โ โ ships in your bundle | โ | no key exists |
| Writes (watchlist, reviews) | user auth required | โ read-only | โ persist, no auth |
| Deterministic data for tests | โ โ live catalogue | โ | โ (?seed=, snapshots) |
| Trigger 500/slow/429 on demand | โ | โ | โ query params |
| Requests | generous | 1,000/day | 10,000/project/day |
Rule of thumb: shipping a real movie product โ TMDB with the key kept server-side. Building the UI, teaching a class, running tests, or keeping a portfolio demo alive โ mock it, then swap the base URL when you're ready (and label the demo data as sample content).
401 status_code 7 and to OMDb 401 "No API key provided."; OMDb's key page lists the free tier as "FREE! (1,000 daily limit)". Check their sites for current terms./m/YOUR_ID/graphql โ same movies, 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 news API, a weather API, any third-party API your app calls, serve a full CRUD REST API, and hand your test suite deterministic fixtures.