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.
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:
// 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:
"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:
- Create an app on the Spotify Developer Dashboard
- Run a one-time OAuth flow to get a refresh token
- Store it in your
.env.local - 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:
| Scenario | What Happens |
|---|---|
| Nothing playing | Shows friendly "not listening" message |
| Private session | Spotify returns null — falls back gracefully |
| Ad playing | No track info available — shows idle state |
| Token expired | Auto-refreshes behind the scenes |
| Network down | SWR retries on reconnect |
| Missing env vars | Logs 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:
{
"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.