Now Playing module showing a Spotify track
This sci-fi art was generated using Nano Banana Pro using custom prompt written by yours truly.

Now Playing: how I wired Spotify into my site

2 min read
EngineeringFrontend

The goal

I wanted a minimal "Now Playing" module that is reliable, lightweight, and easy to reason about. The core idea: use a server route to talk to Spotify, then let the UI poll on a gentle interval. 1

The data flow (server first)

The server route is the only place that touches Spotify. It uses a refresh token to get a short-lived access token, then calls the "currently playing" endpoint.

const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
const NOW_PLAYING_ENDPOINT =
  "https://api.spotify.com/v1/me/player/currently-playing";
 
const getAccessToken = async () => {
  if (!clientId || !clientSecret || !refreshToken) {
    return null;
  }
 
  const basic = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
  const body = new URLSearchParams({
    grant_type: "refresh_token",
    refresh_token: refreshToken,
  });
 
  const response = await fetch(TOKEN_ENDPOINT, {
    method: "POST",
    headers: {
      Authorization: `Basic ${basic}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body,
  });
 
  if (!response.ok) {
    return null;
  }
 
  return response.json();
};

From there, /api/now-playing maps the payload into a small response shape and bails early when nothing is playing.

if (!response || response.status === 204 || response.status > 400) {
  return NextResponse.json({ isPlaying: false });
}
 
const song = await response.json();
 
if (!song?.item) {
  return NextResponse.json({ isPlaying: false });
}

That early return keeps the UI consistent when playback is idle or Spotify is unreachable. 2

The client loop (small and steady)

The UI polls /api/now-playing once on mount and then every 60 seconds. If the request fails, I reset the state back to null and show a clean fallback.

const fetchNowPlaying = async () => {
  try {
    const response = await fetch("/api/now-playing");
    const json = (await response.json()) as NowPlayingSong;
    if (isMounted) {
      setData(json);
    }
  } catch {
    if (isMounted) {
      setData(null);
    }
  }
};

This keeps the UI stable without introducing complex caches or hydration edge cases. 3

UI details I like

  • When a song is playing, I show animated bars to signal activity.
  • When nothing is playing, I show the Spotify glyph and a clean "Not Playing" label.
  • The track title only becomes a link when the API gives me a valid URL.

The goal is a small UI that still feels alive.

Final result (live component)

Not Playing

Spotify

Footnotes

  1. The Spotify endpoint returns 204 when there is no active playback. Treating this as an "idle" state keeps the UI predictable and avoids false error modes.

  2. The server route always returns a consistent shape. That means the client never has to branch on status codes or Spotify-specific edge cases.

  3. A 60 second poll cadence is gentle on the API, aligns with track changes, and avoids excess refresh-token churn.