Blog
Jun 9, 2025 - 6 MIN READ
Displaying Your Spotify Recent Tracks with Nuxt 3

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.

Pheak Minute

Pheak Minute

Showcasing your recently played Spotify tracks on your personal website adds a personal touch that can engage visitors and reflect your music taste. This tutorial will guide you through creating a "Recent Tracks" component for your Nuxt 3 application.

Prerequisites

Before starting, you'll need:

  1. A Spotify Developer account with a registered application
  2. Your Spotify Client ID and Client Secret
  3. A refresh token for persistent authentication
  4. A Nuxt 3 project

If you haven't set up Spotify API access yet, check my previous post on Building a Spotify Now Playing Widget with Nuxt 3 for detailed instructions.

Project Structure

Our implementation will consist of three main components:

  1. Spotify utility functions for authentication and data fetching
  2. A server API endpoint to securely fetch recently played tracks
  3. A Vue component to display the tracks on the frontend

Step 1: Setting Up Spotify Utilities

First, we need to create utility functions for Spotify API authentication and fetching recently played tracks. Create or update your utils/spotify.ts file:

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}`,
    },
  });
};

This utility file handles:

  • Authentication using your Spotify credentials and refresh token
  • Fetching your recently played tracks (limited to 5 for cleaner display)
  • Optionally fetching your currently playing track

Step 2: Creating a Server API Endpoint

Next, create a server API endpoint for fetching recently played tracks at server/api/spotify/recent-tracks.ts:

import { getRecentTracks } from '../../../utils/spotify';

export default defineEventHandler(async (event) => {
  try {
    const response = await getRecentTracks();

    if (response.status === 204 || response.status > 400) {
      return { tracks: [] };
    }

    const data = await response.json();
    
    const tracks = data.items.map((item: any) => {
      const track = item.track;
      return {
        artist: track.artists.map((_artist: any) => _artist.name).join(", "),
        songUrl: track.external_urls.spotify,
        title: track.name,
        albumImageUrl: track.album.images[0]?.url,
        playedAt: item.played_at
      };
    });

    // Set appropriate cache headers
    setResponseHeaders(event, {
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=30'
    });
    
    return { tracks };
    
  } catch (error) {
    console.error("Error fetching recent tracks:", error);
    throw createError({ 
      statusCode: 500, 
      statusMessage: 'Failed to fetch Spotify recent tracks' 
    });
  }
});

This endpoint:

  1. Calls our getRecentTracks utility function
  2. Processes the response to extract relevant track information
  3. Formats the data for our frontend component
  4. Implements caching to optimize performance

Step 3: Creating the Recent Tracks Component

Finally, let's create a Vue component to display the recently played tracks. Create a file at components/RecentTracks.vue or app/components/RecentTracks.vue depending on your project structure:

<script setup lang="ts">
interface Track {
  artist: string;
  songUrl: string;
  title: string;
  albumImageUrl: string;
  playedAt: string;
}

interface RecentTracksResponse {
  tracks: Track[];
}

// Use useFetch with auto-refresh
const { data, error, pending, refresh } = await useFetch<RecentTracksResponse>(
  "/api/spotify/recent-tracks",
  {
    default: () => ({ tracks: [] }),
  }
);

// Manual refresh every 60 seconds
useIntervalFn(() => {
  refresh();
}, 60000);

const tracks = computed(() => data.value?.tracks || []);
const hasTracks = computed(() => tracks.value.length > 0);
</script>

<template>
  <div class="space-y-4">
    <div class="w-full flex flex-row items-center gap-2 mb-6">
      <UIcon
        name="i-heroicons-arrow-top-right-on-square"
        class="h-4 w-4 text-muted-foreground opacity-50 group-hover:opacity-100 transition-opacity"
      />
      <h2 class="text-xl font-semibold">Recently Played</h2>
    </div>

    <UAlert
      v-if="error"
      color="error"
      variant="subtle"
      icon="i-heroicons-exclamation-triangle"
      title="Failed to load recent tracks"
    >
    </UAlert>

    <div v-else-if="pending" class="flex justify-center py-2">
      <UIcon name="i-heroicons-arrow-path" class="animate-spin h-5 w-5" />
    </div>

    <div v-else-if="!hasTracks" class="text-sm text-muted">
      No recently played tracks
    </div>

    <ul v-else class="space-y-3">
      <li v-for="(track, index) in tracks" :key="track.songUrl">
        <Motion
          :initial="{ opacity: 0, y: 10 }"
          :animate="{
            opacity: 1,
            y: 0,
            transition: {
              delay: index * 0.05,
              duration: 0.3,
            },
          }"
        >
          <ULink
            :to="track.songUrl"
            target="_blank"
            rel="noopener noreferrer"
            class="flex items-center gap-3 p-2 rounded-lg hover:bg-elevated transition-colors duration-200"
          >
            <div
              class="relative h-10 w-10 rounded-md overflow-hidden flex-shrink-0"
            >
              <img
                v-if="track.albumImageUrl"
                :src="track.albumImageUrl"
                :alt="`${track.title} album cover`"
                class="h-full w-full object-cover"
              />
              <div
                v-else
                class="h-full w-full bg-muted flex items-center justify-center"
              >
                <UIcon
                  name="i-heroicons-musical-note"
                  class="h-5 w-5 text-muted-foreground"
                />
              </div>
            </div>

            <div class="flex flex-col min-w-0">
              <div class="text-sm font-medium truncate text-highlighted">
                {{ track.title }}
              </div>
              <div class="text-xs text-muted truncate">
                {{ track.artist }}
              </div>
              <div class="text-xs text-muted truncate">
                {{
                  new Date(track.playedAt).toLocaleTimeString([], {
                    hour: "2-digit",
                    minute: "2-digit",
                  })
                }}
              </div>
            </div>

            <UIcon
              name="i-heroicons-arrow-top-right-on-square"
              class="h-4 w-4 text-muted-foreground ml-auto opacity-50 group-hover:opacity-100 transition-opacity"
            />
          </ULink>
        </Motion>
      </li>
    </ul>
  </div>
</template>

This component:

  1. Fetches data from our server API endpoint
  2. Refreshes every 60 seconds to keep the data current
  3. Displays a loading state while fetching
  4. Handles error states gracefully
  5. Renders a beautiful list of recently played tracks with album artwork and artist information
  6. Adds subtle animations for a polished look

Step 4: Environment Configuration

Don't forget to update your Nuxt configuration to include your Spotify credentials. Add these to 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

And update your nuxt.config.ts to include these in your runtime config:

export default defineNuxtConfig({
  // ... other config
  runtimeConfig: {
    // ... other config
    public: {
      // ... other public config
      clientId: process.env.NUXT_PUBLIC_CLIENT_ID,
      clientSecret: process.env.NUXT_PUBLIC_CLIENT_SECRET,
      refreshToken: process.env.NUXT_PUBLIC_REFRESH_TOKEN,
    }
  }
})

Step 5: Using the Component

Finally, you can use the RecentTracks component in any of your pages:

<template>
  <div>
    <h1>My Music Profile</h1>
    <RecentTracks />
  </div>
</template>

Conclusion

With this implementation, you now have a fully functional Recent Tracks component that automatically fetches and displays your listening history from Spotify. The component refreshes periodically to keep your data current and provides a visually appealing way to share your music taste with your website visitors.

This feature adds a personal touch to your site while showcasing modern web development techniques like API integration, server endpoints, and reactive UI components with Nuxt 3.

Feel free to customize the styling to match your site's design system - the component uses UI components from Nuxt UI, but you can adapt it to use any UI library or custom styling.

Not playing
Copyright © 2025