Written up in October 2026. Dates are when I actually built it.
Two days, one weekend project, and a personal rule that it still had to be done properly. Here’s the Capitol Hill Coffee Guide, 42 commits and 233 test cases later.
I wrote a design spec and implementation plan before any code. Sounds slow. It’s why the rest wasn’t.
Treat the Places API like a budget, not a service
Shop data comes from Google Places, which bills per call. The client is a factory with injected dependencies (now, fetch), so every behavior is unit-testable without a clock or a network:
- TTL cache (24 hours by default) per place.
- Daily call cap. Hit it and the client returns stale data instead of failing.
- Per-place backoff. A place that errors gets left alone for 10 minutes before the next try.
- In-flight coalescing, so ten people opening the same shop at once share one upstream call instead of making ten.
- A 3-second timeout on every call.
- Stale fallback everywhere above, so the UI degrades to “slightly old” instead of “broken”.
Don’t build an open proxy by accident
Photos are served through my own endpoint so the API key never touches the browser. The easy mistake is letting any ref through, which turns your server into a free Google Photos relay on your bill. So it does four things: validates the ref against a strict regex, checks it against an allow-list built from my own shop data, snaps the requested width to one of four sizes (so cache keys can’t explode), and has its own daily budget that returns a 503 when spent.
Sorting without lying
Sorting by star rating is dumb: a 5.0 from 24 reviews beats a 4.7 from 1,200. I sort by a Bayesian average, (n*rating + m*prior) / (n + m), with a prior of 4.4 and weight of 50. That puts the 5.0-from-24 at about 4.59 and the 4.7-from-1,200 at about 4.69. The displayed rating is untouched, only the ordering changes.
Honest data, tested UI
Researched attributes (wifi, seating) are marked unconfirmed, and unknown is a real value, never a quiet “no”. Playwright smoke tests run against a sample-data server, and an axe accessibility audit runs inside the end-to-end suite. It forced real fixes: a misused aria-pressed on links, and a nav pill that failed AA contrast.
Deploy is a standalone Next.js bundle built locally and synced to a VPS behind nginx and systemd, so the server only needs Node. On the last day I cut my own “match me” feature to make it a browse-first guide. Better.