A hand-curated guide to independent coffee shops: filterable browsing, a map, detail pages, a visit “passport” and taste-based recommendations. Built and deployed in about 48 hours: 42 commits and 233 test cases.
Architecture
- Next.js, TypeScript, standalone build. The bundle is built and validated locally, then synced to a VPS behind nginx and systemd, so the server only needs Node.
- Data. Curated, researched shop data compiled into the bundle and validated by schema on every deploy, merged with live Google Places data at request time. Researched attributes are flagged unconfirmed, and “unknown” is a first-class value, never a silent “no”.
Interesting problems
Treating a paid API like a budget. The Places client is a factory with injected fetch and clock, so all of it is unit-testable. It has a 24-hour TTL cache, a daily call cap that falls back to stale data, per-place failure backoff (10 minutes), in-flight request coalescing, and a 3-second timeout.
Not building an open proxy. The photo endpoint keeps the API key server-side but validates each photo reference against a regex and an allow-list built from the shop data, snaps requested widths to four sizes to bound cache keys, and enforces its own daily budget (503 when spent).
Honest ranking. Sorting uses a Bayesian average, (n*rating + m*prior) / (n + m) with prior 4.4 and weight 50, so a 5.0 from 24 reviews (about 4.59) ranks below a 4.7 from 1,200 (about 4.69). Displayed ratings are untouched.
Testing and accessibility
Unit and component tests with Vitest, Playwright smoke tests against a sample-data server (no API key needed), and an axe accessibility audit inside the end-to-end suite, which caught a misused aria-pressed on links and a contrast failure on the active nav pill. Built from a written spec and plan, with scope cut on the last day (a “match me” feature) to ship a simpler browse-first guide.