← Back to Blog

Building a Spotify Now Playing Widget with Next.js

6 min read

Building a Spotify Now Playing Widget with Next.js

Adding a "Now Playing" widget to your portfolio is a great way to showcase your personality and technical skills. In this guide, I'll show you how to integrate Spotify's Web API with Next.js to display your currently playing music in real-time.

What We'll Build

A beautiful, animated widget that:

  • Displays your currently playing Spotify track
  • Shows album artwork, song title, and artist
  • Updates automatically every 30 seconds
  • Features smooth animations and dark mode support
  • Links directly to the song on Spotify

Prerequisites

Before we start, you'll need:

  • A Next.js 14+ project with TypeScript
  • A Spotify account (Free or Premium)
  • Basic knowledge of React and API routes
  • Tailwind CSS and Framer Motion installed

Step 1: Set Up Spotify Developer Account

First, we need to create a Spotify application to get API credentials:

  1. Go to the Spotify Developer Dashboard
  2. Click "Create app"
  3. Fill in the details:
    • App name: Your Portfolio
    • Redirect URI: http://localhost:3000/callback
    • Check the Web API checkbox
  4. Save and note your Client ID and Client Secret

Step 2: Get Your Refresh Token

To access your currently playing track, you need a refresh token with the right scope:

  1. Create an authorization URL (replace YOUR_CLIENT_ID):
text
1https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://localhost:3000/callback&scope=user-read-currently-playing
  1. Open this URL in your browser and authorize the app
  2. Copy the code parameter from the redirect URL
  3. Exchange it for a refresh token using curl:
bash
1curl -X POST https://accounts.spotify.com/api/token \
2  -H "Content-Type: application/x-www-form-urlencoded" \
3  -d "grant_type=authorization_code" \
4  -d "code=YOUR_CODE" \
5  -d "redirect_uri=http://localhost:3000/callback" \
6  -d "client_id=YOUR_CLIENT_ID" \
7  -d "client_secret=YOUR_CLIENT_SECRET"
  1. Save the refresh_token from the response

Step 3: Create TypeScript Types

Create src/types/spotify.ts to define our data structures:

typescript
1export interface SpotifyNowPlayingData {
2  isPlaying: boolean;
3  title?: string;
4  artist?: string;
5  album?: string;
6  albumImageUrl?: string;
7  songUrl?: string;
8}
9
10export interface SpotifyTokenResponse {
11  access_token: string;
12  token_type: string;
13  expires_in: number;
14  scope: string;
15}
16
17export interface SpotifyCurrentlyPlayingResponse {
18  is_playing: boolean;
19  item?: {
20    name: string;
21    artists: Array<{ name: string }>;
22    album: {
23      name: string;
24      images: Array<{ url: string; height: number; width: number }>;
25    };
26    external_urls: { spotify: string };
27  };
28}

Step 4: Create Spotify Utility Functions

Create src/utils/spotify.ts for API interactions:

typescript
1import { SpotifyTokenResponse } from "@/types/spotify";
2
3const client_id = process.env.SPOTIFY_CLIENT_ID;
4const client_secret = process.env.SPOTIFY_CLIENT_SECRET;
5const refresh_token = process.env.SPOTIFY_REFRESH_TOKEN;
6
7const basic = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
8const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
9const NOW_PLAYING_ENDPOINT = "https://api.spotify.com/v1/me/player/currently-playing";
10
11export const getAccessToken = async (): Promise<SpotifyTokenResponse> => {
12  const response = await fetch(TOKEN_ENDPOINT, {
13    method: "POST",
14    headers: {
15      Authorization: `Basic ${basic}`,
16      "Content-Type": "application/x-www-form-urlencoded",
17    },
18    body: new URLSearchParams({
19      grant_type: "refresh_token",
20      refresh_token: refresh_token!,
21    }),
22  });
23
24  return response.json();
25};
26
27export const getNowPlaying = async (): Promise<Response> => {
28  const { access_token } = await getAccessToken();
29
30  return fetch(NOW_PLAYING_ENDPOINT, {
31    headers: {
32      Authorization: `Bearer ${access_token}`,
33    },
34  });
35};

Step 5: Create the API Route

Create src/app/api/spotify/now-playing/route.ts:

typescript
1import { NextResponse } from "next/server";
2import { getNowPlaying } from "@/utils/spotify";
3import { SpotifyNowPlayingData } from "@/types/spotify";
4
5export const dynamic = "force-dynamic";
6
7export async function GET() {
8  try {
9    const response = await getNowPlaying();
10
11    if (response.status === 204 || response.status > 400) {
12      return NextResponse.json<SpotifyNowPlayingData>({ isPlaying: false });
13    }
14
15    const song = await response.json();
16
17    if (!song.item) {
18      return NextResponse.json<SpotifyNowPlayingData>({ isPlaying: false });
19    }
20
21    return NextResponse.json<SpotifyNowPlayingData>({
22      isPlaying: song.is_playing,
23      title: song.item.name,
24      artist: song.item.artists.map((a) => a.name).join(", "),
25      album: song.item.album.name,
26      albumImageUrl: song.item.album.images[0]?.url,
27      songUrl: song.item.external_urls.spotify,
28    });
29  } catch (error) {
30    return NextResponse.json<SpotifyNowPlayingData>(
31      { isPlaying: false },
32      { status: 500 }
33    );
34  }
35}

Step 6: Build the React Component

Create src/components/SpotifyNowPlaying.tsx:

typescript
1"use client";
2
3import { useEffect, useState } from "react";
4import { motion, AnimatePresence } from "framer-motion";
5import { Music2 } from "lucide-react";
6import { SpotifyNowPlayingData } from "@/types/spotify";
7
8const SpotifyNowPlaying = () => {
9  const [data, setData] = useState<SpotifyNowPlayingData | null>(null);
10  const [loading, setLoading] = useState(true);
11
12  useEffect(() => {
13    const fetchNowPlaying = async () => {
14      try {
15        const response = await fetch("/api/spotify/now-playing");
16        const nowPlaying: SpotifyNowPlayingData = await response.json();
17        setData(nowPlaying);
18      } catch (error) {
19        console.error("Error fetching now playing:", error);
20      } finally {
21        setLoading(false);
22      }
23    };
24
25    fetchNowPlaying();
26    const interval = setInterval(fetchNowPlaying, 30000);
27
28    return () => clearInterval(interval);
29  }, []);
30
31  if (loading) {
32    return (
33      <div className="animate-pulse flex items-center gap-3">
34        <div className="w-12 h-12 bg-gray-300 dark:bg-gray-700 rounded" />
35        <div className="flex-1">
36          <div className="h-4 bg-gray-300 dark:bg-gray-700 rounded w-3/4 mb-2" />
37          <div className="h-3 bg-gray-300 dark:bg-gray-700 rounded w-1/2" />
38        </div>
39      </div>
40    );
41  }
42
43  if (!data?.isPlaying) {
44    return (
45      <div className="flex items-center gap-3">
46        <Music2 className="w-5 h-5 text-gray-500" />
47        <span className="text-sm text-gray-600">Not playing anything</span>
48      </div>
49    );
50  }
51
52  return (
53    <AnimatePresence mode="wait">
54      <motion.a
55        href={data.songUrl}
56        target="_blank"
57        rel="noopener noreferrer"
58        whileHover={{ scale: 1.02 }}
59        className="flex items-center gap-3 p-4 bg-gradient-to-r from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 rounded-lg border border-green-200 dark:border-green-800"
60      >
61        {data.albumImageUrl && (
62          <img
63            src={data.albumImageUrl}
64            alt={data.album}
65            className="w-12 h-12 rounded"
66          />
67        )}
68        <div className="flex-1 min-w-0">
69          <p className="text-sm font-semibold text-gray-900 dark:text-gray-100 truncate">
70            {data.title}
71          </p>
72          <p className="text-xs text-gray-600 dark:text-gray-400 truncate">
73            {data.artist}
74          </p>
75        </div>
76      </motion.a>
77    </AnimatePresence>
78  );
79};
80
81export default SpotifyNowPlaying;

Step 7: Add Environment Variables

Create a .env.local file in your project root:

env
1SPOTIFY_CLIENT_ID=your_client_id_here
2SPOTIFY_CLIENT_SECRET=your_client_secret_here
3SPOTIFY_REFRESH_TOKEN=your_refresh_token_here

⚠️ Important: Never commit .env.local to version control!

Step 8: Use the Component

Import and use the component anywhere in your app:

typescript
1import SpotifyNowPlaying from "@/components/SpotifyNowPlaying";
2
3export default function Home() {
4  return (
5    <div>
6      <h1>Welcome to my portfolio</h1>
7      <SpotifyNowPlaying />
8    </div>
9  );
10}

Customization Ideas

Here are some ways to enhance your widget:

  1. Add an animated equalizer when music is playing
  2. Show listening history using the recently played endpoint
  3. Display top tracks from your Spotify profile
  4. Add playback controls (requires additional scopes)
  5. Create a full music dashboard with stats and insights

Troubleshooting

Widget shows "Not playing anything":

  • Verify your environment variables are correct
  • Make sure you're actively playing music on Spotify
  • Check that your refresh token has the user-read-currently-playing scope

API returns 401 Unauthorized:

  • Your refresh token may have expired - regenerate it
  • Double-check your Client ID and Client Secret

Widget not updating:

  • Check the browser console for errors
  • Verify the API route is accessible at /api/spotify/now-playing

Performance Considerations

  • The widget polls every 30 seconds - adjust based on your needs
  • Consider implementing SWR or React Query for better caching
  • Use Next.js Image component for optimized album art loading
  • Add error boundaries to prevent crashes

Conclusion

You now have a fully functional Spotify "Now Playing" widget! This is a great addition to any portfolio, showing both your technical skills and personality.

The same pattern can be applied to other APIs like:

  • Last.fm for music scrobbling
  • GitHub for recent activity
  • Wakatime for coding stats
  • Strava for fitness activities

Happy coding, and enjoy sharing your music taste with the world! 🎵

Resources

© 2025 Agus Narestha | Made With ❤️