Building a Dynamic Spotify Currently Playing Widget with TypeScript, React, and Next.js
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:
- Create an app in your spotify dashboard — We only need the
Web API
- Copy your
Client ID
andClient Secret
- Add your
Redirect URI
— For this tutorial, we’ll be usinghttp://localhost:3124/callback
- Save your tokens in your
.env.local
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:
- Obtain an access token.
- Make the required call to the Spotify endpoint.
- Parse the response.
- 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:
- Call the
currently-playing
endpoint. - Parse the response.
- 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:
- Call the
currently-playing
endpoint - Check the response status
- If it’s a
401
- Invalidate the token cache
- Fetch a new token by calling the token endpoint.
- Cache the new token
- Retry the same `currently-playing` request
- If it’s a 200
- 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!