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 addhttp://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>
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 tocode
. -
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 useuser-read-currently-playing
anduser-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
This should redirect you to something like this:
http://localhost:3000/?code=<code>
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
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>
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
}
};
This code does the following:
- Declares or imports variables we need from
.env
per Vite documentation. - 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. - Use the fetch API (which is vanilla JS) to send a POST request to our
token_endpoint
, with the requiredheaders
and request body parameters (underbody
). 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 ofgrant_type: 'authorization_code'
.
-
Note: we already have our Refresh Token, so we use
- Use
.then()
method (docs) to convert the response to json. - 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},
}
}
- Fetches an access_token from
access_token.json
. - Sends a GET request to the
now_playing_endpoint
with our access_token as authorization. - 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. - 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.
- 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},
}
}
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>
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>
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
stoleadapted 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>
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)