How a small Express app keeps title search working when its main API is down or doesn't know a game: two keyless sources, a hard 3 second timeout each, and a cache that refuses to remember outages.
NXTO Game Backlog is a free game backlog tracker. You type a title, pick the game, it lands in your list. The catalog (about 153,000 games) comes mostly from RAWG, and RAWG is the weak link: its free tier has a hard quota, it has outages, and it lags on new releases. When search breaks, a new user can't add their first game, and that's the one moment I can't afford to lose them.
So search now has a fallback that answers when RAWG is down or doesn't know the title.
Two sources that need no API key
- Steam store search knows every PC release the day it appears. It returns soundtracks, DLC and demos next to games, so each hit is checked with
appdetails, and onlytype === 'game'survives. That same call gives genres, release year and a short description. - HowLongToBeat adds playtime, and covers console-only games Steam has never heard of.
Results are merged by normalized title into the same shape the normal search returns, so the frontend doesn't know which path answered. Playtime from HLTB is attached to the Steam entry with the same title. HLTB titles Steam doesn't have are kept only if they are close enough to the query (similarity of at least 0.75), because below that they are usually unrelated games.
Never let a slow source hold the search up
Both sources run in parallel with Promise.allSettled, each with a 3 second timeout (AbortSignal.timeout on the fetch, plus a Promise.race on the whole call as a second belt). A source that times out or fails contributes an empty list, and the other one still answers.
const [steamRes, hltbRes] = await Promise.allSettled([ withTimeout(searchSteam(query)), withTimeout(searchHltb(query)), ]) const steam = steamRes.status === 'fulfilled' ? steamRes.value : [] const hltb = hltbRes.status === 'fulfilled' ? hltbRes.value : [] Results are capped at 6, and the Steam cover falls back to the header image when the portrait art 404s, which it does for most small indies.
The cache bug I avoided on purpose
Answers are cached for an hour per normalized query. The detail that matters: a result is only cached when both sources succeeded. If Steam was down and I cached the thin answer, the outage would stick for an hour after Steam came back. A failed source means "answer now, remember nothing".
What I'd tell myself earlier
- Decide up front what "degraded" looks like. For me it's fewer results and no playtime, never an error page.
- Bound every external call. Without the timeout, a hanging provider turns a partial failure into a total one.
- Cache successes, not outages.
NXTO is free to try at nxto.app, and if you track your backlog somewhere else I'd like to know what search feels like there.

