Back to blogs

May 29, 2026

How to Add Spotify Currently Listening to a Portfolio

A step-by-step guide to adding a Spotify currently playing and recently played widget to a Next.js portfolio.

Next.jsSpotifyPortfolio

What we are building

I wanted to show the song I am currently listening to on my portfolio. If I am not playing anything, the widget should show my most recently played track instead.

The full setup has two parts:

  1. Get permission from Spotify and save the access and refresh tokens.
  2. Use those tokens in a Next.js API route to fetch the current or recent track.

Create a Spotify app

Go to the Spotify Developer Dashboard and create a new app.

After creating the app, copy the Client ID and Client Secret. You will need both of them when requesting tokens from Spotify.

Set the redirect URI to:

http://127.0.0.1:3000/callback

The redirect URI must match exactly in every place: the Spotify dashboard, the authorization URL, and the token request.

Spotify developer dashboard app credentials and redirect URI

Ask Spotify for permission

Next, open an authorization URL in the browser. This asks Spotify for permission to read data from your account.

For a currently listening widget, these two scopes are enough:

user-read-currently-playing
user-read-recently-played

You can check all available scopes in the Spotify scopes documentation.

Use this URL after replacing YOUR_CLIENT_ID:

https://accounts.spotify.com/authorize?response_type=code&client_id=YOUR_CLIENT_ID&scope=user-read-recently-played%20user-read-currently-playing&redirect_uri=http%3A%2F%2F127.0.0.1%3A3000%2Fcallback&state=someRandomState

Spotify will ask you to approve the app. After approval, it redirects to your redirect URI with a code in the URL.

It will look similar to this:

http://127.0.0.1:3000/callback?code=AUTHORIZATION_CODE&state=someRandomState

Copy the code value. We will exchange it for an access token and a refresh token.

Spotify redirect URL with code query parameter

Exchange the code for tokens

Now send a POST request to Spotify's token endpoint. You can do this with Postman, Insomnia, Bruno, curl, or any API client.

Postman request for exchanging Spotify authorization code for tokens

Here is the curl request:

curl --location "https://accounts.spotify.com/api/token" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --header "Authorization: Basic BASE64_CLIENT_ID_AND_CLIENT_SECRET" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=AUTHORIZATION_CODE_FROM_REDIRECT_URL" \
  --data-urlencode "redirect_uri=http://127.0.0.1:3000/callback"

The Authorization header uses this format:

Basic base64(client_id:client_secret)

Do not publish your real client secret, access token, refresh token, or cookies in a blog post or GitHub repo.

The response will look like this:

{
  "access_token": "ACCESS_TOKEN",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "REFRESH_TOKEN",
  "scope": "user-read-currently-playing user-read-recently-played"
}

Save both tokens. The access token is used to call Spotify APIs. The refresh token is used to get a new access token when the old one expires.

In my project, I save them in MongoDB:

await SpotifyToken.updateOne(
  { tokenType: "access" },
  { token: accessToken, tokenType: "access" },
  { upsert: true },
);

await SpotifyToken.updateOne(
  { tokenType: "refresh" },
  { token: refreshToken, tokenType: "refresh" },
  { upsert: true },
);

Create the API route

The browser should not talk to Spotify directly because that would expose the token. Instead, my portfolio has a Next.js API route at /api/spotify.

import { getCurrentPlaying } from "@/lib/spotify/get-current-playing";
import { NextResponse } from "next/server";

export const dynamic = "force-dynamic";

export async function GET() {
  try {
    const data = await getCurrentPlaying();

    if (!data) {
      return NextResponse.json(null, { status: 200 });
    }

    return NextResponse.json(data);
  } catch (error) {
    console.error("Failed to load Spotify track:", error);
    return NextResponse.json(null, { status: 200 });
  }
}

I return null with a 200 status when something fails. Spotify is a nice extra on the portfolio, so it should not break the whole page.

Get the currently playing track

To get the current track, I call this Spotify endpoint:

https://api.spotify.com/v1/me/player/currently-playing

In my code, it looks like this:

const currentlyPlayingUrl =
  "https://api.spotify.com/v1/me/player/currently-playing";

let currentlyPlayingResponse = await fetch(currentlyPlayingUrl, {
  headers: {
    Authorization: `Bearer ${accessToken.token}`,
  },
});

Spotify can return a few different responses:

When the request succeeds, I return only the data my UI needs:

return {
  album: item.album.name,
  albumImage: item.album.images[0].url,
  artists: item.album.artists.map((artist) => artist.name).join(", "),
  name: item.name,
  songUrl: item.external_urls.spotify,
  isPlaying: currentlyPlayingData.is_playing,
  trackId: item.id,
  previewUrl: item.preview_url,
};

Fall back to the recently played track

If Spotify returns 204, there is no song currently playing. In that case, I fetch the latest recently played track instead.

This is the endpoint:

https://api.spotify.com/v1/me/player/recently-played?limit=1

And this is the request from my code:

const recentlyPlayedUrl =
  "https://api.spotify.com/v1/me/player/recently-played?limit=1";

const recentlyPlayedResponse = await fetch(recentlyPlayedUrl, {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
  cache: "no-store",
});

This makes the widget useful even when I am not actively listening to anything. The UI can show Currently Playing when isPlaying is true and Recently Played when it is false.

Refresh the access token

Spotify access tokens expire after some time. When Spotify returns 401, I use the refresh token to get a new access token.

This is the token refresh request:

const response = await fetch("https://accounts.spotify.com/api/token", {
  method: "POST",
  headers: {
    "Content-Type": "application/x-www-form-urlencoded",
    Authorization: `Basic ${Buffer.from(
      `${clientId}:${clientSecret}`,
    ).toString("base64")}`,
  },
  body: new URLSearchParams({
    grant_type: "refresh_token",
    refresh_token: refreshToken.token,
  }),
});

After Spotify returns the new access token, I update it in the database:

await SpotifyToken.updateOne(
  { tokenType: "access" },
  { token: newAccessToken },
);

Spotify may also return a new refresh token. If it does, I save that too:

if (data.refresh_token) {
  await SpotifyToken.updateOne(
    { tokenType: "refresh" },
    { token: data.refresh_token },
  );
}

Then I retry the Spotify request with the new access token.

Render the widget in the portfolio

On the frontend, my Spotify component calls my own API route:

const fetcher = async (url: string) => {
  const res = await fetch(url, { cache: "no-store" });
  if (!res.ok) return null;
  return res.json();
};

const { data, isLoading } = useSWR("/api/spotify", fetcher, {
  revalidateOnFocus: false,
  revalidateOnReconnect: false,
  revalidateIfStale: false,
});

Then the UI renders the track name, album art, artist, and status.

{data?.isPlaying ? (
  <span>Currently Playing</span>
) : (
  <span>Recently Played</span>
)}

That is the complete flow: authorize once, save the tokens, refresh the access token when needed, and expose a small /api/spotify route for the portfolio UI.