Mood Music: Describe Your Mood, Get Real Spotify Playlists

|  6 min read
ai claude spotify cloudflare workers
View the project

Building a Claude-powered mood-to-playlist finder on Cloudflare Workers — search-phrase generation, round-robin ranking, and why a single Worker can serve both the app and the API.

Building a Claude-powered mood-to-playlist finder on Cloudflare Workers — search-phrase generation, round-robin ranking, and why a single Worker can serve both the app and the API.

The idea #

Spotify’s own recommendation engine is good at predicting what you’ll listen to next based on history. It’s not built for “I want music that matches exactly how I feel right now, in a sentence I just typed” — a rainy Monday where you need to focus, a breakup week, a day that’s actually going great for once. Those are moods, not listening histories, and translating a mood into the right search terms is a small but genuinely hard language problem. That’s the gap Mood Music fills: type a mood or a line about your day, get back real, existing Spotify playlists that fit it.

How it works #

A mood or description goes to the API, which sends it to Claude with a forced tool-call so the response comes back as structured JSON rather than freeform text. Claude’s job isn’t to pick songs — it’s to translate the mood into a handful of good search phrases, the kind you’d type into Spotify yourself if you were better at articulating a vibe in three words. Each phrase then gets paged through Spotify’s search endpoint (50 results per page, up to 4 pages), and the results are merged round-robin across phrases — so the strongest hit from every phrase surfaces before any single phrase’s second-tier results do — until there are at least 50 unique playlists to show.

Logging in with Spotify isn’t required to search. It only unlocks one extra thing: a history of your past mood searches, tied to your account.

Architecture #

The whole thing is a pnpm monorepo with two apps, deployed together as a single Cloudflare Worker:

apps/
├── web/   Vite + React + TanStack Router + nuqs (the SPA)
└── api/   Hono running on Cloudflare Workers

In production, the Worker serves the built SPA’s static files and runs the API from the same domain — Cloudflare’s assets binding routes /api/* to the Worker and everything else to the static build. That means no CORS to think about and cookie-based sessions just work, since the browser only ever talks to one origin. Locally, Vite proxies /api/* to wrangler dev to preserve that same property during development.

Data storage: KV and Durable Objects, not a database #

There’s deliberately no traditional database here. Two Cloudflare primitives cover everything:

  • Workers KV does double duty by key prefix. session:* maps a session cookie to a Spotify user ID. cache:mood:* caches a mood search’s full result — Claude’s interpretation plus the playlists found — for 24 hours, keyed by a hash of the normalized mood text. If two people (or the same person twice) search a near-identical mood within a day, neither Claude nor Spotify gets hit a second time.
  • Durable Objects give each Spotify user their own dedicated, strongly-consistent storage instance — one UserState object per user, addressed by their Spotify user ID. It holds their OAuth tokens (with auto-refresh handled internally) and their full mood-search history. Anonymous visitors get the same treatment for rate-limiting purposes, addressed by IP instead of a Spotify ID.

That per-user Durable Object is also how the free tier is enforced: logged-in users get 10 fresh Claude calls per rolling 7 days on the app’s shared API key, tracked inside their own UserState object, with an option to add your own Anthropic key from the profile menu to bypass the limit entirely. Cache hits don’t count against the quota at all, since they never touch Claude.

Design details worth calling out #

A few smaller decisions that mattered more than their size suggests:

  • Redirect URI resolution is per-request, not hardcoded. The Spotify OAuth callback URL is picked from the incoming Referer header against an allowlist of known origins, rather than a single static value — so the same deployment works correctly whether it’s reached through a custom domain or the default workers.dev URL.
  • The app-only Spotify token (for anonymous search) is cached in module scope per Worker isolate. No shared cache needed — it’s cheap enough to refetch on a cold start that adding Durable Object or KV overhead for it wasn’t worth it.
  • It’s an installable PWA , with a Workbox-generated service worker that precaches the app shell for offline loading — deliberately scoped to only run in production builds so it doesn’t fight with Vite’s dev server.
  • Scope is intentionally narrow. The Spotify OAuth request only asks for identity (user-read-email user-read-private) — the app never reads or writes a user’s library or playlists, only searches Spotify’s public catalog.

A few things to know before you try it #

You might not be able to sign in with Spotify — and that’s Spotify, not a bug. Every new Spotify integration starts out in what Spotify calls Development Mode , and since Spotify tightened these rules in February 2026 , Development Mode apps can only be signed into by a small allowlist the developer manages by hand — five accounts, down from the 25 it used to allow — and the app owner has to keep an active Premium subscription just to keep the app working at all. Moving an app fully out of Development Mode requires Extended Quota Mode , and Spotify’s current bar for that is a legally registered business running an active commercial service with at least 250,000 monthly active users and a presence in Spotify’s key markets. That’s an enterprise requirement, not a weekend-project one, so Mood Music is going to stay in Development Mode indefinitely. In practice: searching for a mood works for everyone, logged in or not, because search runs on an app-level Spotify token that this restriction doesn’t touch. Signing in — which only exists to save your search history — will only work if your Spotify account is on my hand-picked allowlist.

Why only 10 searches a week. Every fresh mood search is a real call to the Claude API, and those calls cost money per token. Ten fresh searches per person per rolling week, billed to the app’s own Anthropic key, is what keeps this free to run without me having to take it down. It doesn’t apply to repeats — if your mood text matches something searched in the last 24 hours, you get the cached result and it doesn’t touch your quota at all. If you want more, you can drop your own Anthropic API key into the profile menu and search against your own budget instead of mine.

Or just self-host it. The code is fully open source, and self-hosting removes both limits at once — your own Spotify app means your own allowlist (or your own shot at Extended Quota Mode, if you ever qualify), and your own Anthropic key means your own quota, set however you like. Setup is documented in the repo’s README — a Spotify Developer app, a Cloudflare account, and a handful of environment variables is basically the whole list.

Resources #

Live: moodmusic.site

Code: github.com/chikeozulumba/mood-music