Building a Dynamic Spotify Currently Playing Widget with TypeScript, React, and Next.js

Daniel Voigt
5 min readFeb 2, 2024

A few years ago, I wrote a quick blog post on creating a ‘Now Playing’ Widget for your Gatsby site. Sadly, this relied on the Netlify Graph feature, which, as of May 1st, 2023, has been deprecated. So, let’s switch gears to Next.js using Server Components!

Server Components fit perfectly here! Users get fast static HTML all without the hassle of dealing with potential leaks of secret tokens or writing API routes. Plus, we can manage caching and keep a tight grip on our API limits.

Setup

To use the Spotify API, follow these straightforward steps:

Generating a refresh token

For our Widget, we’ll be utilizing the authorization code flow . You can manually generate the token as specified or write a small script. Just for fun and easier access if we ever change the token, let’s create a quick script using fastify (who still uses express?). Here’s a brief rundown:

Setting up our imports and checking if all necessary tokens are in the environment.

Our initial route redirects to the Spotify authorize endpoint. I’ve chosen additional scopes as I use the token not only for the now-playing widget but also for a list of songs I recently listened to.

Our callback endpoint is where Spotify will notify us if the user (we, in this scenario) grants access to the data.

Starting the Fastify server and opening our login path. After running the script and authorizing the app, we can add the obtained token to our environment as SPOTIFY_REFRESH_TOKEN.

Coding the API Access

The process here is pretty straightforward. We just need to:

  1. Obtain an access token.
  2. Make the required call to the Spotify endpoint.
  3. Parse the response.
  4. Return the data

Get an Access Token

To kick things off, let’s import all our tokens. The beauty of using Server Components in this context, is that we exclusively operate on the server, allowing us to import tokens from our environment without the worry of any potential leaks!

To get an access token is straightforward. We just call the token endpoint with our refresh token in exchange for an access token.

I’m a very strong supporter of Parse, don’t validate. So we use Zod to parse the response. Our schema looks like this:

Zod automatically ignores any unspecified fields, allowing us to focus only on the ones we need.

The “magic” part is the use of fetch.

Next.js extends the native Web fetch() API to allow each request on the server to set its own persistent caching semantics.

With next: { revalidate: 3600, tags: [TOKEN_CACHE_TAG] }, we instruct Next.js on how to cache the response of our request. Subsequent requests will retrieve the cached response without directly querying Spotify. The tag becomes valuable when we need to prompt Next.js to clear the cache for this specific request!

We set the revalidation interval to every 3600 seconds, aligning with the lifetime of our access token. If you’re familiar with Tanstack Query, this will feel very intuitive. Importantly, this approach eliminates the need for a global state variable to store the token, we can simply call getAccessToken whenever needed and get a cached value!

Call the Respective Spotify Endpoint

This is pretty much the same as the getAccessToken logic:

  1. Call the currently-playing endpoint.
  2. Parse the response.
  3. Cache and return the response.

To start, we refer to the Get Currently Playing Track specification and create a schema with all relevant fields we’re interested in:

I split them into two schemas, since the track_schema is the same for a lot of endpoints, so we can easily reuse it.

I’ve settled on a revalidate interval of 30 seconds. This seems like a good middle ground. We get somewhat live updates but don’t hog our API limits! We either return our parsed data or the response code.

Handling Access Code Revalidation

You may have noticed that we pass the token to this function instead of directly calling getAccessToken inside it. I also chose a somewhat strange name for this function. This is because I wanted to have a robust solution for handling our access token.

To ensure our access token is always valid, I implemented a solution for handling potential invalidation:

  1. Call the currently-playing endpoint
  2. Check the response status
  3. If it’s a 401
  4. Invalidate the token cache
  5. Fetch a new token by calling the token endpoint.
  6. Cache the new token
  7. Retry the same `currently-playing` request
  8. If it’s a 200
  9. Just pass a long the response

This is generic for all our requests, not only currently-playing. To streamline this process, let’s write a higher-order function to handle all this.

Our higher-order Function takes two arguments

  • f: Any function that accepts the access token as its first argument, followed by any number of additional arguments …rest: P
  • revalidateCall: An optional argument to identify whether we are in a revalidation call. This is crucial to prevent potential infinite loops if the access token remains invalid.

We are generic over the input arguments of f so we can implement functions which take some arguments. Consider something like this:

const getRecentHitsFetcher = async (token: string, limit: number = 5) 

The response type for our fetcher is Promise<T | number>. The fetcher should consistently return either the parsed response or the status of the request.

When the fetcher returns a number, indicating an invalid status code, we check if it’s a 401 Unauthorized. In such cases, we call revalidateTag(TOKEN_CACHE_TAG). This is a function Next provides to invalidate the cache by any tag. That’s why we supplied the tags: [TOKEN_CACHE_TAG] option in our getAccessToken handler earlier! Subsequently, we call the function recursively. The next invocation of getAccessToken will provide the new (and cached) access token, ensuring the subsequent call to f succeeds. You can think of it as invalidateQueries from TanStack Query and the tag as a queryKey !

Currently Playing Component

With most of the work completed, it’s time to render the result! I chose TailwindCSS and shadcn, but feel free to choose the tools you’re most comfortable with.

import { getCurrentlyPlaying } from "@/lib/spotify";

Simply import our function from our Spotify library.

One component for when we are not listening to any song currently. This is indicated by a 204 response! Which is not really well documented.

Another component comes is used during the initial render. With the core component utilizing an async function, React renders a placeholder component while the promise is unresolved. Once the actual component is ready, React streams the HTML to the frontend, replacing the placeholder.

Our core component for the widget! We call our getCurrentlyPlaying function and render either our offline, placeholder or core component! We can now easily place our Widget anywhere we like, just don’t forget the Suspense boundary!

Further Information

To get a broader view on implementing additional endpoints and/or how this would look in an actual codebase feel free to take a look at the source of my website source! Or in action here.

I’ll leave it as an exercise for the reader to implement better error handling!

--

--

Daniel Voigt

Software developer, language enthusiast and Jazz musician.