·4 min read

spotify now playing widget.

A real-time widget that shows what I'm currently listening to on Spotify — built with Next.js API routes, SWR polling, and smart caching.

spotifyapinextjsreactswr

Spotify Now Playing Widget

Ever notice how music says a lot about someone? I wanted my portfolio to go beyond the usual "I like music" bullet point and actually show what's playing right now. So I built a real-time Spotify widget that sits right on the homepage.

It turned out to be one of my favorite personal touches — visitors get a little peek into my current vibe, and it's sparked some genuinely fun conversations.

Live Demo

Here's the actual widget running live — not a screenshot, the real thing pulling data from Spotify right now.

Why Build This?

I code with music on basically all the time, so it felt natural to surface that. But beyond the personal touch, it was a fun engineering puzzle:

  • Stay authenticated forever without manual token refreshes
  • Respect Spotify's API limits with smart server-side caching
  • Handle every edge case — private sessions, ads, paused playback, network hiccups
  • Keep it fast — zero impact on page load times

How It Works

The system has three layers, each keeping things clean and efficient.

1. Backend API Route

A Next.js API route at /api/spotify/now-playing handles all the Spotify communication:

typescript
// Server-side cache to reduce Spotify API calls
const CACHE_TTL = 30000; // 30 seconds
 
async function getAccessToken(refreshToken: string) {
  const basic = Buffer.from(
    `${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`
  ).toString("base64");
 
  const res = await fetch("https://accounts.spotify.com/api/token", {
    method: "POST",
    headers: {
      Authorization: `Basic ${basic}`,
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: refreshToken,
    }),
  });
 
  return res.json();
}

The key insight here is server-side caching with a 30-second TTL. Instead of hammering Spotify's API on every visitor, the server caches the response and serves it to everyone. In production, that takes API calls from ~2,880/day down to about 48/day — a 98% reduction.

2. Frontend Component

The React component uses SWR for data fetching, which gives me automatic polling, caching, and background updates essentially for free:

typescript
"use client";
 
import useSWR from "swr";
 
export default function NowPlaying() {
  const { data } = useSWR("/api/spotify/now-playing", fetcher, {
    refreshInterval: 30000,      // poll every 30s
    refreshWhenHidden: false,    // save bandwidth when tab is hidden
    revalidateOnReconnect: true, // catch up after network drops
  });
 
  // Render playing state or fallback...
}

A small detail I'm happy with: the component is visibility-aware. It only polls when the tab is active, so it's not wasting bandwidth in the background.

3. One-Time OAuth Setup

This was the trickiest part, but you only do it once. The flow goes:

  1. Create an app on the Spotify Developer Dashboard
  2. Run a one-time OAuth flow to get a refresh token
  3. Store it in your .env.local
  4. The system handles access token renewal forever after

Pro tip: The refresh token never expires unless you revoke app access. Set it once, forget about it.

The Edge Cases

Building a widget that depends on an external API means things will go wrong. Here's how I handle it:

ScenarioWhat Happens
Nothing playingShows friendly "not listening" message
Private sessionSpotify returns null — falls back gracefully
Ad playingNo track info available — shows idle state
Token expiredAuto-refreshes behind the scenes
Network downSWR retries on reconnect
Missing env varsLogs clear error, returns safe fallback

The philosophy is simple: never show a broken UI. Every failure state has a graceful fallback.

Performance

A few optimizations that keep this lightweight:

  • Server-side cache shared across all visitors (not per-user)
  • SWR's deduplication prevents multiple components from making duplicate requests
  • No layout shift — the widget reserves its space during loading with a skeleton
  • Next.js Image component handles responsive images and lazy loading

Dependencies

Pretty slim — nothing exotic:

json
{
  "swr": "^2.3.6",
  "next": "^15.5.4",
  "react": "^19.0.0",
  "react-icons": "^5.5.0"
}

What I Learned

The best portfolio features aren't always the flashiest — they're the ones that feel real. This widget doesn't have fancy animations or complex UI, but when someone visits and sees I'm listening to the same artist they love? That's an instant connection.

Also, SWR is genuinely great for this kind of polling use case. The visibility-aware refresh alone saved me from writing a bunch of custom IntersectionObserver logic.