A random picker looks like a one-line problem:
const randomMovie = movies[Math.floor(Math.random() * movies.length)];
That line is fine when you control a clean in-memory array. The problem changes when the data comes from a live, paginated API and the user expects every visible filter to be honored.
Now the picker has to deal with incomplete metadata, regional streaming availability, recent repeats, image-loading state, and a candidate pool too large to fetch in full. “Pick a random one” becomes a bounded search pipeline with a weighted lottery at the end.
The examples below come from a movie picker built with React, TypeScript, TanStack Start SSR, and Cloudflare Workers, but the same decisions apply to product pickers, prompt generators, playlist shufflers, and any tool that selects one item from a constrained remote dataset.
AI assistance was used during drafting. The implementation details in this version were checked against the current source code.
Randomness starts after the constraints
The naive snippet assumes that every item in movies is already valid. A live movie API does not give you that guarantee.
A user might ask for:
- an action or adventure movie;
- released during one of several selected periods;
- rated at least 7.0;
- no longer than two hours;
- available through Netflix or Disney+ in the United States;
- available by subscription rather than only to rent or buy.
The remote catalog can still return entries without a poster, overview, usable runtime, matching watch offer, or a final status of Released. Uniformly choosing from the raw response would make the output random, but not necessarily usable.
So the real operation is closer to this:
validated request
-> bounded discovery
-> usable candidate pool
-> recent-history exclusion
-> weighted selection
-> full-detail validation
-> one result
The random choice is one stage in the system, not the system itself.
Treat client presets as requests, not authority
The browser sends filters to a server-side adapter. The adapter validates supported services, availability modes, genre IDs, year ranges, ratings, runtimes, and recent movie IDs before constructing a TMDB request. The TMDB read token stays in the Worker environment and never enters the client bundle.
Preset pages need an additional boundary. For example, /random/netflix sends a controlled netflix scope. The server maps that scope to US Netflix subscription availability and rejects attempts to combine it with conflicting service or availability parameters.
The themed /random/christmas page uses a controlled collection instead of a provider scope. Its server-side mapping supplies the approved Christmas keywords, and additional title-and-overview checks keep unrelated keyword matches out of the pool.
That distinction matters. Hiding a control in React does not enforce a product rule. If the rule affects which items are eligible, the server has to own it.
Build a bounded candidate pool
TMDB discovery results are paginated, so the picker cannot start with one complete array of every possible movie. It has to build a useful pool without turning one click into hundreds of upstream requests.
The implementation uses two paths:
- When the filtered result has at most 10 accessible pages, read all of those pages.
- When it has more than 10 accessible pages, sample 5 distinct pages from at most the first 500 accessible pages.
The fetched pages are flattened and screened before selection. Candidates need a poster and overview at the discovery stage. The full-detail stage later confirms that the movie:
- has a runtime of at least 60 minutes;
- has a final status of
Released; - is not a concert, live performance, stand-up special, or another excluded non-feature type;
- has at least one US watch offer matching the requested service and availability mode.
TV Movie is always excluded. Documentary and Music are excluded by default, but become eligible when the user explicitly selects those genres.
This is an important product boundary: the output is not a statistically uniform draw across the entire TMDB catalog. It is a bounded, filtered selection from live search results.
Weight the lottery without turning it into a top-ten list
After building the pool, the server still has to select one candidate. Giving every candidate identical odds is simple, but it ignores the difference between a rating backed by thousands of votes and the same rating backed by a very small sample.
The picker combines two normalized signals:
// Simplified pseudocode that mirrors the current weighting formula.
const voteConfidence = voteCount / (voteCount + 500);
const popularityPercentile = popularityRank / (candidateCount - 1);
const weight = minRating >= 8
? 0.2 + 0.6 * voteConfidence + 0.2 * popularityPercentile
: 0.3 + 0.5 * voteConfidence + 0.2 * popularityPercentile;
voteConfidence rises as more people rate a movie but approaches a ceiling. popularityPercentile is relative to the candidates in the current sampled pool. When the user explicitly asks for an 8+ rating, vote confidence receives slightly more influence.
This does not sort the pool and return the highest-scoring title. The computed value is a probability weight in a random draw. Less-established candidates can still win; they simply do not receive identical odds by default.
The selected candidate then goes through full-detail validation. A failed candidate is removed, and the weighted draw continues with the remaining pool. The loop stops after at most eight detail attempts, returning a clear failure instead of retrying indefinitely.
Avoid repeats without creating an account system
Short-term repetition is a different problem from weighting. A perfectly reasonable random process can still produce the same item twice, so the browser keeps a capped local history rather than asking users to create accounts.
The client stores up to 50 recent movie IDs in localStorage and sends them as an exclusion list. The server removes those IDs from the usable candidate pool before performing the weighted draw.
Recent genre history is also stored locally for one narrow balancing rule. On generator variants where that rule is enabled, if the user has not chosen a genre and at least two of the last five results contained Animation, the next request sends avoidAnimation=1. The server then excludes Animation for that request unless Animation is already required or explicitly selected.
This is conditional exclusion, not a vague “diversity score” and not a permanent preference profile.
There is one more edge case: a fully materialized small pool can be exhausted by recent history. In that case, the server reuses the candidate pool it already fetched, retries without the exclusion list, and marks the response as historyRefreshed. The client resets its local cycle and records the new result. No second discovery request is needed.
If browser storage is unavailable, generation still works. The user simply loses the short-term deduplication behavior.
Asset readiness is part of the state machine
The API can finish before the interface is visually ready. A movie result that arrives quickly can still feel broken if the winning poster appears half-loaded during the reel animation.
The UI starts warming a small reel-poster pool when the active preset or poster category changes. Those decoded images can be used for the looping phase while the movie request is in flight.
After the server returns the winning movie, the client separately prepares its poster before entering the settling phase. It waits for a ready, timeout, or error result rather than treating “URL received” as “image ready.” A timeout or temporary preload error does not erase the valid poster URL; the result card can still attempt to load it normally.
Only after that preparation step does the pending result enter the reel, allowing the motion to slow onto the actual winner instead of revealing a partially decoded image halfway through the transition.
Users who prefer reduced motion receive a static poster state and a short transition rather than the looping and settling reel.
The general lesson is that perceived performance depends on asset state as well as network response time. If the visual result is part of the interaction, image readiness belongs in the state machine.
What this design taught me
Several lessons generalize beyond movie data:
- Client-side presets are not enforcement boundaries. Validate every eligibility rule on the server.
- Describe randomness precisely. Sampling, filtering, weighting, and recent-history exclusion all change what “random” means.
- Use the smallest memory that solves the problem. A capped browser history can reduce repeats without accounts, profiles, or a database.
- Separate discovery from validation. Cheap summary filtering can build the pool; expensive detail checks can be limited to a bounded number of candidates.
- Treat visual assets as asynchronous state. A successful JSON response does not mean the result is ready to animate.
The live Random Movie Generator is the reference implementation that motivated these constraints. The interesting part is not the movie domain itself, but how quickly a one-line random choice becomes a system of explicit contracts.
If you were building a filtered picker, would you give every valid candidate equal odds, or use a weighted lottery? More importantly, how would you explain that choice to your users?

Top comments (1)
Your breakdown of transforming a simple random picker into a robust system that respects user filters is insightful. The challenge of managing incomplete data from an API while ensuring a quality user experience is something many developers face. It might be beneficial to consider caching strategies for frequently accessed data to reduce load times and API requests, especially for popular genres or filters. If you're looking for help refining the candidate pool logic or optimizing the API interactions, I'd be glad to explore a paid collaboration. How have you approached error handling when certain filters lead to no valid results?