
Building a Spotify Now Playing Widget with Nuxt 3
How to integrate the Spotify API with Nuxt 3 to display your currently playing track in real-time on your personal website or portfolio.

Pheak Minute
Adding a "Now Playing" widget to your personal website is a great way to personalize your online presence and showcase your music taste. In this tutorial, I'll show you how to integrate the Spotify API with Nuxt 3 to display your currently playing track in real-time.
Setting Up Spotify API Access
Before diving into code, you'll need to set up your Spotify Developer account and create an application:
- Visit the Spotify Developer Dashboard and log in
- Create a new application
- Set a redirect URI (e.g.,
http://localhost:3000/callback
) - Note down your
client_id
andclient_secret
- Generate a refresh token (we'll use this for persistent access)
Project Structure
For our implementation, we'll need three main components:
- Utility functions for Spotify API authentication and data fetching
- A server API endpoint to securely fetch the currently playing track
- A Vue component to display the widget on the frontend
Step 1: Creating Spotify Utility Functions
First, let's create utility functions to handle authentication and API requests. Create a file named utils/spotify.ts
:
const config = useRuntimeConfig();
const client_id = config.public.clientId;
const client_secret = config.public.clientSecret;
const refresh_token = config.public.refreshToken;
const basic = Buffer.from(`${client_id}:${client_secret}`).toString("base64");
const TOKEN_ENDPOINT = "https://accounts.spotify.com/api/token";
const RECENT_TRACKS_ENDPOINT =
"https://api.spotify.com/v1/me/player/recently-played?limit=5";
const NOW_PLAYING_ENDPOINT =
"https://api.spotify.com/v1/me/player/currently-playing";
export const getAccessToken = async () => {
const response = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: {
Authorization: `Basic ${basic}`,
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refresh_token as string,
}),
});
return response.json();
};
export const getRecentTracks = async () => {
const { access_token } = await getAccessToken();
return fetch(RECENT_TRACKS_ENDPOINT, {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
};
export const getNowPlaying = async () => {
const { access_token } = await getAccessToken();
return fetch(NOW_PLAYING_ENDPOINT, {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
};
The code above handles three main functions:
getAccessToken
: Authenticates with Spotify using our client credentials and refresh tokengetRecentTracks
: Fetches our recently played tracks (optional feature)getNowPlaying
: Fetches what's currently playing on our Spotify account
Step 2: Creating a Server API Endpoint
Next, let's create a server API endpoint to securely fetch the currently playing track. Create a file named server/api/spotify/now-playing.ts
:
import { getNowPlaying } from "../../../utils/spotify";
export default defineEventHandler(async (event) => {
try {
const response = await getNowPlaying();
if (response.status === 204 || response.status > 400) {
return { isPlaying: false };
}
const song = await response.json();
if (!song.item) {
return { isPlaying: false };
}
const isPlaying = song.is_playing;
const title = song.item.name;
const artist = song.item.artists
.map((_artist: any) => _artist.name)
.join(", ");
const album = song.item.album.name;
const albumImageUrl = song.item.album.images[0]?.url;
const songUrl = song.item.external_urls.spotify;
const progress = song.progress_ms;
const duration = song.item.duration_ms;
// Set appropriate cache headers
setResponseHeaders(event, {
"Cache-Control": "public, s-maxage=60, stale-while-revalidate=30",
});
return {
album,
albumImageUrl,
artist,
isPlaying,
songUrl,
title,
progress,
duration,
};
} catch (error) {
console.error("Error fetching now playing:", error);
throw createError({
statusCode: 500,
statusMessage: "Failed to fetch Spotify data",
});
}
});
This endpoint:
- Calls our
getNowPlaying
utility function to fetch data from Spotify - Handles various states (not playing, errors)
- Processes and structures the response data
- Implements caching for performance optimization
- Returns the formatted data for our frontend component
Step 3: Creating the Frontend Component
Finally, let's create a Vue component to display the currently playing track. Create a file named components/NowPlaying.vue
:
<script setup lang="ts">
interface NowPlayingResponse {
isPlaying: boolean;
title?: string;
artist?: string;
albumImageUrl?: string;
songUrl?: string;
progress?: number;
duration?: number;
}
// Use useFetch with auto-refresh
const { data, error, pending, refresh } = await useFetch<NowPlayingResponse>(
"/api/spotify/now-playing",
{
default: () => ({
isPlaying: false,
title: undefined,
artist: undefined,
albumImageUrl: undefined,
songUrl: undefined,
progress: undefined,
duration: undefined,
}),
server: false, // Only run on client side
}
);
// Manual refresh every 10 seconds
useIntervalFn(() => {
refresh();
}, 10000);
const progressPercentage = computed(() => {
const progress = data.value?.progress || 0;
const duration = data.value?.duration || 1;
return (progress / duration) * 100;
});
const isPlaying = computed(() => data.value?.isPlaying || false);
</script>
<template>
<Motion
:initial="{
scale: 1.1,
opacity: 0,
filter: 'blur(20px)',
}"
:animate="{
scale: 1,
opacity: 1,
filter: 'blur(0px)',
}"
:transition="{
duration: 0.6,
delay: 0.1,
}"
>
<div
v-if="error || pending || !data || !data.isPlaying"
class="flex items-center gap-2 text-xs text-muted"
>
<UIcon name="i-heroicons-musical-note" class="w-3.5 h-3.5" />
<span>Not playing</span>
</div>
<ULink
v-else
:to="data?.songUrl || '#'"
target="_blank"
rel="noopener noreferrer"
class="group"
>
<div
class="flex items-center gap-3 bg-muted/50 backdrop-blur-md p-1.5 pr-3 rounded-full hover:bg-muted transition-all duration-300 border border-neutral-200/40 dark:border-neutral-700/40"
>
<!-- Album Cover with Animation -->
<div
class="relative h-7 w-7 rounded-full overflow-hidden flex-shrink-0"
:style="{
animation: isPlaying ? 'spin 15s linear infinite' : 'none',
}"
>
<div
class="absolute inset-0 bg-black/20 backdrop-blur-[1px] z-10 rounded-full"
/>
<img
v-if="data?.albumImageUrl"
:src="data.albumImageUrl"
:alt="data.title || 'Album cover'"
class="h-full w-full object-cover"
/>
<div
class="absolute inset-[30%] bg-elevated backdrop-blur-sm z-20 rounded-full flex items-center justify-center"
>
<UIcon
v-if="isPlaying"
name="i-heroicons-pause"
class="w-2.5 h-2.5 text-highlighted"
/>
<UIcon
v-else
name="i-heroicons-play"
class="w-2.5 h-2.5 text-highlighted"
/>
</div>
</div>
<!-- Track Info -->
<div class="flex flex-col min-w-0 max-w-[120px] sm:max-w-[160px]">
<div class="text-xs font-medium truncate text-highlighted">
{{ data?.title }}
</div>
<div class="text-[10px] text-muted truncate">
{{ data?.artist }}
</div>
</div>
<!-- Progress Bar -->
<div class="w-10 sm:w-12 h-1 rounded-full bg-muted overflow-hidden">
<div
:style="{ width: `${progressPercentage}%` }"
class="h-full bg-primary-500 transition-all duration-300"
/>
</div>
</div>
</ULink>
</Motion>
</template>
<style scoped>
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
This component:
- Defines a TypeScript interface for our API response
- Uses Nuxt's
useFetch
composable to call our API endpoint - Implements auto-refreshing every 10 seconds to keep the display updated
- Calculates the progress percentage for the progress bar
- Handles different states (playing, not playing, loading, error)
- Renders a beautiful UI with album art that spins while playing
- Includes a progress bar that updates in real-time
Step 4: Environment Configuration
To keep our API credentials secure, we need to set up environment variables. Create or update your .env
file:
NUXT_PUBLIC_CLIENT_ID=your_spotify_client_id
NUXT_PUBLIC_CLIENT_SECRET=your_spotify_client_secret
NUXT_PUBLIC_REFRESH_TOKEN=your_spotify_refresh_token
Then, update your nuxt.config.ts
file to expose these variables to our application:
export default defineNuxtConfig({
runtimeConfig: {
public: {
clientId: process.env.NUXT_PUBLIC_CLIENT_ID,
clientSecret: process.env.NUXT_PUBLIC_CLIENT_SECRET,
refreshToken: process.env.NUXT_PUBLIC_REFRESH_TOKEN,
},
},
// rest of your Nuxt config
});
Step 5: Using the Component
Now you can use the component anywhere in your application:
<template>
<div>
<h2>Currently Listening To</h2>
<NowPlaying />
</div>
</template>
Technical Considerations and Optimizations
Caching Strategy
Our implementation includes server-side caching via the Cache-Control
header. This prevents excessive API calls to Spotify while still keeping the display relatively fresh. We've set a 60-second cache with a 30-second stale-while-revalidate window.
Authentication Flow
We're using a refresh token for authentication, which means we don't need user interaction to keep the token fresh. The refresh token will work indefinitely unless revoked.
Performance Optimizations
- The component only refreshes every 10 seconds to minimize API calls
- We're using client-side hydration to avoid unnecessary server load
- The progress bar updates smoothly via CSS transitions
Conclusion
With these components in place, you now have a beautiful, real-time Spotify "Now Playing" widget on your Nuxt 3 website. This integration showcases not only your music taste but also demonstrates how to work with third-party APIs in a Nuxt application.
The solution we've built is efficient, secure, and visually appealing. You can further customize the styling to match your website's design or add additional features like recently played tracks or favorite artists.
What music integrations would you like to see next? Let me know in the comments below!
Displaying Your Spotify Recent Tracks with Nuxt 3
How to implement a Spotify Recent Tracks component in your Nuxt 3 application to showcase your music listening history.
Mastering Time Handling in TypeScript with Moment.js
A practical guide to working with dates and times in TypeScript applications using the powerful Moment.js library, with real-world utility functions.