I Accidentally Built a Microservice for a Spotify Widget

8 min read
Cloudflare WorkersNext.jsSpotify APIEdge ComputingOAuthTypeScript

I recently added a Spotify "Now Playing" component to my portfolio website at tejaspathak.tech.

The goal was simple.

Show my currently playing Spotify track if music is active. Otherwise show the last played song.

At first this looked like a pretty straightforward feature.

Since my portfolio is built with Next.js, the easiest solution would have been using an API route like:

/api/spotify

Inside it I could fetch data from the Spotify Web API and return JSON to the frontend.

That would have worked perfectly fine.

I still ended up building a separate edge microservice for it using Cloudflare Workers.

Why I avoided Next.js API routes

The main reason was architectural separation.

I wanted the Spotify integration to behave like an isolated service instead of being tightly coupled to my frontend application.

Cloudflare Workers were interesting to me because they:

In practice this means the Worker runs closer to the user geographically instead of depending on a single fixed server region.

For something tiny like a Spotify widget, this is absolutely overkill.

But it was also a good excuse to learn:

The final architecture looked like this:

Spotify Widget Architecture

Migrating from npm to pnpm

Before building the Worker, I migrated my portfolio project from npm to pnpm.

A big reason was package isolation and stricter dependency behavior.

pnpm blocks untrusted build scripts by default and uses a content-addressable dependency store instead of duplicating massive node_modules folders everywhere.

Migration was surprisingly simple:

pnpm import
pnpm install

Then I removed:

After migration the project used:

pnpm-lock.yaml

instead.

Setting up the Worker

Instead of creating a completely separate backend repository, I kept the Worker inside the same repo.

The structure looked like this:

portfolio/
├── app/
├── components/
├── workers/
│   └── spotify-worker/

Technically this is not a full monorepo yet because the Worker still has its own lockfile and dependency tree, but it follows a similar organizational pattern.

I initialized the Worker using:

pnpm create cloudflare@latest spotify-worker

and selected:

After deployment using:

pnpm wrangler deploy

Cloudflare generated a public .workers.dev URL which became the API endpoint for the frontend.

Spotify OAuth setup

The Worker needs three things from Spotify:

The Client ID and Secret come directly from the Spotify Developer Dashboard.

The refresh token requires completing the OAuth flow manually.

First I generated an authorization URL:

https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://127.0.0.1:8787/callback&scope=user-read-currently-playing%20user-read-recently-played

After authenticating with Spotify, it redirected to:

http://127.0.0.1:8787/callback?code=XXXX

I copied the code parameter and exchanged it for tokens using:

curl -X POST "https://accounts.spotify.com/api/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-u "CLIENT_ID:CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code=YOUR_CODE \
-d redirect_uri=http://127.0.0.1:8787/callback

Spotify returned:

The refresh token was then stored securely using Cloudflare Worker secrets:

pnpm wrangler secret put SPOTIFY_CLIENT_ID
pnpm wrangler secret put SPOTIFY_CLIENT_SECRET
pnpm wrangler secret put SPOTIFY_REFRESH_TOKEN

This keeps sensitive credentials completely outside the frontend.

Worker implementation

The Worker itself is pretty small.

I used TypeScript interfaces to strongly type:

The frontend expects data in this format:

interface SpotifyTrackResponse {
  isPlaying: boolean;
  title: string;
  artist: string;
  album: string;
  albumImageUrl: string;
  songUrl: string;
  playedAt: string | null;
  progressMs: number;
  durationMs: number;
}

The Worker flow looks like this:

  1. Use the refresh token to request a short-lived Spotify access token
  2. Fetch the currently playing track
  3. If nothing is playing, fetch the most recently played track
  4. Normalize the response shape
  5. Return clean JSON to the frontend

Handling rate limits properly

One important part was handling Spotify rate limits correctly.

I added a utility function with exponential backoff support for HTTP 429 responses.

Whenever Spotify returns a Retry-After header, the Worker pauses before retrying the request.

Without this, repeated polling from the frontend could eventually trigger stricter rate limiting.

Fetching the currently playing track

The Worker first checks active playback:

const currentlyPlayingUrl =
  'https://api.spotify.com/v1/me/player/currently-playing';

If Spotify returns:

204 No Content

the Worker falls back to:

const recentlyPlayedUrl =
  'https://api.spotify.com/v1/me/player/recently-played?limit=1';

This makes the component feel responsive even when nothing is actively playing.

Edge caching strategy

I used different cache durations depending on playback state.

For active playback:

max-age=5

For recently played tracks:

max-age=60

Active playback changes constantly, while recently played songs are relatively stable.

This reduces unnecessary API requests while keeping the UI close to real-time.

Final frontend component

Once the Worker API was working, the frontend only needed to fetch JSON from the deployed Worker URL.

The component itself ended up becoming a vinyl-inspired animated player UI with:

Ironically the frontend part turned out to be simpler than the infrastructure behind it.

For a Spotify widget, I think this was definitely over-engineered.

The final result

Loading...