DEV Community

Cover image for Using Spotify Web API with SvelteKit Endpoints to create a Now Playing component.
Koen
Koen

Posted on

Using Spotify Web API with SvelteKit Endpoints to create a Now Playing component.

Link to original post: https://www.koenraijer.io/blog/spotify-api

Ever since I saw this implementation of the Spotify API in Next.js (check the footer), I've been wanting to do the same thing in SvelteKit. Knowing the song someone's listening to in that very moment makes an otherwise mostly static website feel pretty dynamic.

So, I resumed my quest into the world of API's, requests and endpoints from where I left off: Fetching from a public API with SvelteKit Endpoints.

I learned a lot about HTTP methods, SvelteKit endpoints and I learned about Postman as an easy way to test API requests. Scattered throughout this post are many links to resources that helped me figure things out. They might be of interest to you as a refresher or as a starting point for further learning.

0.1 What are we going to do?

We are going to use the Authorization Code Flow to get access to the Spotify Web API. We'll use our access to get data about the track we're listening to in that moment, as well as our 20 top tracks of the past half year. We'll do that using SvelteKit endpoints, which will be run server-side as serverless functions. The data will be accessible to our frontend using those endpoints. We'll fetch it and display it nicely in a Svelte component.

This is what that's going to look like:

0.2 Creating a Spotify app

Follow this tutorial to create your own Spotify app.

  • edit settings and add http://localhost:3000 to Redirect URIs. Make sure to click Add, and then save at the bottom of the pop-up.

Notice the Client ID and the Client Secret (for which you first have to press Show Client Secret), on the app overview page.

0.3 Storing variables in .env

Create src > .env. Add your client ID and client secret. Make sure you prefix the name with VITE_ (otherwise you can't import them locally). We will later pass these to Vercel as environment variables to Vercel, meaning they won't be exposed to the client.

// .env
VITE_SPOTIFY_CLIENT_ID=<clientid>
VITE_SPOTIFY_CLIENT_SECRET=<clientsecret>
Enter fullscreen mode Exit fullscreen mode

We will come back to this later. First, let's take a look at the Authorization Code Flow.

0.4 Authorization Code Flow

This image provides a pretty clear overview of the steps needed to get data from the Spotify Web API.
It shows that we need to:

  • Authorize in order to get a code
  • Use that code to get a refresh token
  • Use that refresh token to get an access token
  • Use the access token to get our data

1.0 Requesting user authorization

We should send a GET request to https://accounts.spotify.com/authorize with the following parameters:

  • client_id (we know this already)
  • response_type: must set this to code.
  • redirect_uri: http://localhost:3000 (same as app settings)
  • scope: determines what you'll be able to be authorized to request. For the tutorial, we'll use user-read-currently-playing and user-top-read. Scopes should be separated by spaces. Check out other authorization scopes.

Since we only need to do this once to login and get our code, we'll just construct the request by hand and paste it in our browser. Fill in your client ID in the following url and paste in your browser (all other parameters have already been filled in).

// Browser
https://accounts.spotify.com/authorize?client_id=<clientid>&response_type=code&redirect_uri=http%3A%2F%2Flocalhost:3000&scope=user-read-currently-playing%20user-top-read
Enter fullscreen mode Exit fullscreen mode

This should redirect you to something like this:

http://localhost:3000/?code=<code>
Enter fullscreen mode Exit fullscreen mode

Save the code for now. We'll use it to get a Refresh Token.

2.0 Getting a Refresh Token

Next, we'll use the command-line tool cURL to send a POST request to the Spotify API Token endpoint. We pass it our request headers using -H, and our request body parameters using -d (Check out this list of curl options if you're interesed).

// Terminal
curl -H "Authorization: Basic <base64 encoded client_id:client_secret>"
-d grant_type=authorization_code -d code=<code> -d redirect_uri=http%3A
%2F%2Flocalhost:3000 https://accounts.spotify.com/api/token
Enter fullscreen mode Exit fullscreen mode

You'll need to add the following:

  • The code we got earlier.
  • A Base64 encoded string with your client ID and secret in the format clientid:clientsecret. You can create one here.

Paste the resulting command in your terminal / cmd prompt and press enter.
What you'll get back should hold your Refresh Token in JSON format.

We'll add the Refresh Token to our .env file as VITE_SPOTIFY_REFRESH_TOKEN. The .env file should now look like this:

// .env
VITE_SPOTIFY_CLIENT_ID=<clientid>
VITE_SPOTIFY_CLIENT_SECRET=<clientsecret>
VITE_SPOTIFY_REFRESH_TOKEN=<refreshtoken>
Enter fullscreen mode Exit fullscreen mode

3.0 Getting an Access Token

Our Refresh Token is valid indefinitely, but we can't use it to request data from the Spotify API. For that, we'll need an Access Token. Each Access Token is only valid for 1 hour, so we'll need to request them programmatically.

So, create src > routes > api > access_token.json.js. This will be an endpoint that exports a request handler function which sends a POST request to the correct Spotify endpoint and returns our Access Token.

Note: this endpoint sends a POST request, but the result can be accessed through a GET endpoint. This confused me at first.

// access_token.json.js
const client_id = import.meta.env.VITE_SPOTIFY_CLIENT_ID;
const client_secret = import.meta.env.VITE_SPOTIFY_CLIENT_SECRET;
const refresh_token = import.meta.env.VITE_SPOTIFY_REFRESH_TOKEN;
const redirect_uri = "http://localhost:3000/"
const token_endpoint = `https://accounts.spotify.com/api/token`;

export const get = async () => {
    const { access_token } = await fetch(token_endpoint, {
        method: 'POST',
        headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
        },
        body: new URLSearchParams({
            grant_type: 'refresh_token',
            refresh_token,
            redirect_uri,
            client_id,
            client_secret,
        })
    }).then(res => res.json());

    return {
        body: access_token
    }
};
Enter fullscreen mode Exit fullscreen mode

This code does the following:

  1. Declares or imports variables we need from .env per Vite documentation.
  2. Export an async function called post, which runs everytime the URL of the endpoint is called with the POST method. We'll do exactly that later, when we need an Access Token.
  3. Use the fetch API (which is vanilla JS) to send a POST request to our token_endpoint, with the required headers and request body parameters (under body). You can find what they are exactly under "Request Access Token" at Authorization Code Flow.
    • Note: we already have our Refresh Token, so we use grant_type: 'refresh_token' instead of grant_type: 'authorization_code'.
  4. Use .then() method (docs) to convert the response to json.
  5. Store the result as a constant using const { access_token }. This syntax uses object destructuring.

4.0 Use our access token to send requests to the Spotify Web API

The data on the now playing song and top tracks are probably better off in separate endpoints. We don't need the top tracks every time we update our now playing song, and vice versa.

So, we'll be creating two endpoints: now_playing.json.js, top_tracks.json.js.

4.1 now_playing endpoint

Create src > routes > api > now_playing.json.js, which will send a GET request and return data on the track we're currently playing.


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

export async function get() {
    const {access_token} = await fetch('http://localhost:3000/api/access_token.json').then(res => res.json())

    const res = await fetch(now_playing_endpoint, {
        headers: {
            Authorization: `Bearer ${access_token}`
        }
    })

    if (res.status === 204 || res.status > 400) {
        return {body: { isPlaying: false }}
    }

    const song = await res.json();
    const isPlaying = song.is_playing;
    const title = song.item.name;
    const artist = song.item.artists.map((_artist) => _artist.name).join(', ');
    const album = song.item.album.name;
    const albumImageUrl = song.item.album.images[0].url;
    const songUrl = song.item.external_urls.spotify;

    return {
    body: {title, artist, album, isPlaying, albumImageUrl, songUrl},
    }
}
Enter fullscreen mode Exit fullscreen mode
  1. Fetches an access_token from access_token.json.
  2. Sends a GET request to the now_playing_endpoint with our access_token as authorization.
  3. Checks if the response has a status of 204 or >400. This is what the API will return if you're not using Spotify. We don't want the error message though, because we want it to say that you're not currently listening. So we intercept the error message and return {isPlaying: false} instead.
  4. We then go into the response object and create several constants.
    • Note: for the artist, we go over the array of artists (as there can be multiple), and join them together in a comma-separated string.
  5. Return all the variables we created.

You should now be able to see your newly created endpoint at http://localhost:3000/api/now_playing.json.

4.2 top_tracks endpoint

Create src > routes > api > top_tracks.json.js. The code is very similar to the now_playing endpoint.


const top_tracks_endpoint = `https://api.spotify.com/v1/me/top/tracks`;

export async function get() {
    const {access_token} = await fetch('http://localhost:3000/api/access_token.json').then(res => res.json())

    const data = await fetch(top_tracks_endpoint, {
        headers: {
            Authorization: `Bearer ${access_token}`
        }
    }).then(res => res.json());

    return {
        status: 200,
        body: {top_tracks: data.items},
    }
}
Enter fullscreen mode Exit fullscreen mode

See it live at http://localhost:3000/api/top_tracks.json!

4.3 An aside about dealing with API responses

In the above code, you can see I return top_tracks as data.items. I know I need data.items because I looked at what I got back from the request, using the following code:

<pre>{JSON.stringify(data, null, 2)}</pre>
Enter fullscreen mode Exit fullscreen mode

Check out JSON.stringify() on MDN for more information.

5.0 Creating our NowPlaying component

Create src > lib > components > NowPlaying.svelte. This will be a component that calls the now_playing.json endpoint and displays the song we're currently playing. It uses Tailwind CSS for styling.

<script>
    import {onMount} from 'svelte';
    let song = {}

    async function getNowPlaying() {
        song = await fetch('/api/now_playing.json').then(res => res.json())
    }

    onMount(async () => {
        getNowPlaying();
    })

    setInterval(() => {getNowPlaying()}, 5000);
</script>

<div class="text-sm w-fit text-gray-500 bg-gray-100 border-2 p-4 pl-5 py-2 rounded-lg">

    {#if song.isPlaying}
        <div class="audio relative m-0 p-0 mr-1 top-0 inline bg-gray-500">
            <span></span>
            <span></span>
            <span></span>
        </div>

        <span class="leading-0"><span class="font-semibold ml-4"><a href={song.songUrl} rel="noopener noreferrer" target="_blank" class="hover:underline">{song.title}</a></span> - {song.artist}</span>

    {:else}
        <svg class="inline h-4 w-4 top-0 -translate-y-[1.5px]" fill="currentColor" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg"><path d="M16 0c-8.803 0-16 7.197-16 16s7.197 16 16 16c8.803 0 16-7.197 16-16s-7.12-16-16-16zM23.36 23.12c-0.319 0.479-0.881 0.64-1.36 0.317-3.76-2.317-8.479-2.797-14.083-1.52-0.557 0.165-1.037-0.235-1.199-0.72-0.156-0.557 0.24-1.036 0.719-1.197 6.084-1.36 11.365-0.803 15.521 1.76 0.563 0.24 0.64 0.88 0.401 1.36zM25.281 18.719c-0.401 0.563-1.12 0.803-1.683 0.401-4.317-2.641-10.88-3.437-15.916-1.839-0.641 0.156-1.365-0.161-1.521-0.803-0.161-0.64 0.156-1.359 0.797-1.52 5.844-1.761 13.041-0.876 18 2.161 0.484 0.24 0.724 1.041 0.323 1.599zM25.443 14.24c-5.125-3.043-13.683-3.36-18.563-1.839-0.801 0.239-1.599-0.24-1.839-0.964-0.239-0.797 0.24-1.599 0.959-1.839 5.683-1.681 15.041-1.359 20.964 2.161 0.719 0.401 0.957 1.36 0.557 2.079-0.401 0.563-1.36 0.801-2.079 0.401z"/></svg>
        <strong>Not playing</strong> - <span class="text-gray-500">Spotify</span>
    {/if}

    <br>

    <a href="/dashboard" sveltekit:prefetch class="text-xs text-primary hover:underline flex justify-end text-right"><svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 inline-block bottom-0 translate-y-[0.45rem]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="3">
    <path stroke-linecap="round" stroke-linejoin="round" d="M9 5l7 7-7 7" />
    </svg>Top tracks</a>

</div>

<style>

.audio span {
    width: 4px;
    height: 100%;
    border-radius: 2px;
    position: absolute;
    bottom: .25rem;
    @apply bg-gray-500;
}

.audio span:first-of-type {

margin-top: 0;

}

.audio span:nth-child(1) {

animation: animationTest 1.5s infinite ease-in-out;

left: 0;

}

.audio span:nth-child(2) {

animation: animationTest 1.75s infinite ease-in-out;

left: 5px;

}

.audio span:nth-child(3) {

animation: animationTest 2s infinite ease-in-out;

left: 10px;

}

@keyframes animationTest {

0% { height: 2px; }

50% { height: 14px; }

100% { height: 2px; }

}

</style>
Enter fullscreen mode Exit fullscreen mode

I posted this mostly, so you can be inspired to create your own custom component. You have the data, so make something cool with it!

  • It uses setInterval to make new requests every 5 seconds. I considered using song length and song progress (both of which are available), but that seemed overly complicated and error-prone.
  • It uses Svelte {#if} syntax to display the currently playing song, or "Not playing - Spotify" based on whether I am playing a song or not.
  • The currently playing song has a CSS animation mimicing sound waves that I stole adapted from this codepen. That accounts for all the contents of the <style tag.

6.0 Creating the Dashboard

Create src > routes > dashboard.svelte. This page will fetch our top tracks from our top_tracks.json endpoint.

<script context="module">
    export const load = async ({fetch}) => {
        const top_tracks = await fetch('./api/top_tracks.json').then(res => res.json())

        return {
            props: top_tracks
        }
    }
</script>

<script>
    export let top_tracks;
</script>

<h1 class="text-3xl mb-2 text-base">Top tracks</h1>
<p class="mb-8">Curious what I'm currently jamming to? Here's my top tracks on Spotify updated daily.</p>
<div>
    {#each top_tracks.top_tracks as track, index}
        <div class="grid grid-cols-4 content-start">
            <div class="col-span-3">    
                <a class="font-semibold text-lg" href={track.external_urls.spotify} rel="noopener noreferrer" target="_blank">{index + 1}. {track.name}</a>
                <span>{track.artists[0].name}</span>
            </div>
            <img class="mb-4 mt-2 h-20 w-20" alt="{track.name}'s album cover" height={track.album.images[1].height} width={track.album.images[1].width} src={track.album.images[1].url}>
        </div>
        <hr class="mt-2 mb-8">
    {/each}
</div>
Enter fullscreen mode Exit fullscreen mode

7.0 Room for improvement

I am sure there are several ways this code can be improved. These are some of the things I already thought of:

  • Shadow endpoints could be used if you don't plan on adding more stuff to the dashboard. They make load boilerplate unnecessary in many cases, but they haven't been added to the documentation so far.
  • I'm probably lacking in the error handling department. It's something I plan on learning more about.
  • The number of API requests can be reduced by using the expires_in variable in when requesting a new Access Token. It's better to check whether the old Access Token works, and if not, request a new one.
  • Perhaps there's a more elegant solution to getting the now playing song, instead of an API request every 5 seconds?
  • Using Svelte's {#await} syntax display "Loading..." instead of Not playing.

If you have suggestions to improve my code, found errors or have other comments please don't hesitate to tell me πŸ˜‰.

Top comments (0)