Home Home Blog How to consume Spotify APIs using Next.js and JavaScript June 1, 2022 • Estimated reading time: 12 min read
next.js spotify javascript apis
Finally, another article in my (poor 😓) blog. Sometimes I don't know what to say, or what to explain you. But in this case, I show you how I've integrated Spotify API's into my website.
As you can see in this section I made 2 sections showing you my the Last Songs I Played , the Top Artists and my Top Tracks .
v12.1.5
Is the image above clear? (not for me)
Now, Spotify has a lot types of Authentication flows (read more ). I'll use the Authorization Code Flow , I'll run the flow locally because the only person who can access my API is me.
Bro are you serious? Do I have explain you how to create a new application? I'm a developer, not a philosopher.
The only thing I can say is that you need add (into the Redirect URIs your localhost:port
for development). I think it's enough.
When you're done, you'll have a new application, a new Client ID
and a new Client Secret
, required for the next step.
Personally, I prefer to store the Client ID
and Client Secret
in a .env
file, but you can use any other way.
# Spotify
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_REFRESH_TOKEN=
SPOTIFY_REDIRECT_URI=
The first step is to compose the URL to request the authorization. It is comopsed by the following parameters:
https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${REDIRECT_URI}&scope=${SCOPES}
In my case I've indicated as ${REDIRECT_URI}
my localhost:port
for development (keep in mind that you need to add this in the Redirect URIs section on Spotify Dashboard).
user-read-currently-playing
user-read-recently-played
user-read-playback-state
user-read-playback-position
user-top-read
playlist-read-collaborative
playlist-read-private
Visit the composed URL. After you're done, you'll have a new URL with the code
parameter in the Redirect URI you've indicated.
The following step is to encode the ${CLIENT_ID}:${CLIENT_SECRET}
variables in Base64.
> echo -n "${CLIENT_ID}:${CLIENT_SECRET}" | base64
When you have encrypted the ${CLIENT_ID}
and the ${CLIENT_SECRET}
, sperated by :
, you can compose the URL to request the authorization that we will use in the next step.
curl -H "Authorization: Basic ${CLIENT_PARAMETERS_IN_BASE64}" -d grant_type=authorization_code -d code=${AUTHORIZATION_CODE} -d redirect_uri=http%3A%2F%2Flocalhost:3000 https://accounts.spotify.com/api/token
The response will be something like this:
{
"access_token" : "ACCESS_TOKEN" ,
"token_type" : "Bearer" ,
"expires_in" : 3600 ,
"refresh_token" : "REFRESH_TOKEN" ,
"scope" : "playlist-read-private playlist-read-collaborative user-read-playback-state user-read-currently-playing user-read-recently-played user-read-playback-position user-top-read"
}
The access_token
allows you to access the API data.
A simple example of how to use the access_token
to get the current user's data:
curl -X "GET" "https://api.spotify.com/v1/me/player/currently-playing" -H "Accept: application/json" -H "Content-Type: application/json" -H "Authorization: Bearer ${ACCESS_TOKEN}"
{
timestamp: 1657357734036 ,
context: {
external_urls: {
spotify: 'https:
} ,
href: 'https:
type: 'playlist',
uri: 'spotify: playlist: 5yIhKIf3Hf3g8HOLNGYWeM'
} ,
progress_ms: 9490 ,
item: {
album: {
album_type: 'album',
artists: [ Array] ,
available_markets: [ Array] ,
external_urls Object
Finally, with that introduction, we can start with the funny part.
In this section, on my website, I've created a Player Component tho show the current track I'm playing on Spotify. This is how it looks like:
For that component I've used the following packages, libs and assets:
I've created an API on Next.JS to fetch the current track.
app/api/spotify/current-listening
import { getCurrentlyListening } from 'lib/spotify' ;
import { normalizeCurrentlyListening } from 'lib/utils/normalizers' ;
export default async function handler ( req, res ) {
const response = await getCurrentlyListening ( ) ;
if ( ! response) {
return res. status ( 500 ) . json (
The getCurrentlyListening()
is a simple method that takes the access token and calls the Spotify API.
export const getCurrentlyListening = async ( ) => {
const nowPlayingEndpoint = 'https://api.spotify.com/v1/me/player/currently-playing' ;
const { access_token : accessToken } = await getAccessToken ( ) ;
if ( ! accessToken) {
return ;
}
return fetch ( nowPlayingEndpoint, {
headers : {
The getAccessToken()
just resolve the access token, stored in the session or refreshed by the refresh_token
flow.
lib/utils/normalizers/normalizeSpotify.js
export const normalizeCurrentlyListening = ( { is_playing, progress_ms, item } ) => ( {
id : item. id ,
isPlaying : is_playing,
title : item. name ,
artist : item. artists ?. map ( ( { name } ) => name) . join ( ' ' ) ,
album : item. album ?. name,
item images url
After the API is ready, I can use it to fetch the current track. For that, I've created a contenxt to manage current playing song .
import s from 'styles/pages/home.module.css' ;
import useSWR from 'swr' ;
import { Player } from 'components/spotify' ;
import { useUI } from 'components/ui/ui-context' ;
export default function HomePage ( { article } ) {
const { setSpotifyListening } = useUI ( ) ;
const fetcher = url =>
fetch ( url
The motivation for manage the current song via a context
it's because I need that data in other components. So I can use it in the Player
component and in others.
And this is the Player component:
components/spotify/player.tsx
import s from './player.module.css' ;
import React , { useMemo } from 'react' ;
import Link from 'next/link' ;
import Lottie from 'react-lottie-player' ;
import PlayerJson from 'lib/lottie-files/player.json' ;
import { ChevronUp , Spotify } from 'components/icons' ;
import { useUI } from 'components/ui/ui-context' ;
import
Like the previous section, I've created a dedicated API to manage the fetch
.
app/api/spotify/recently-played.tsx
import config from 'lib/config' ;
import { getRecentlyPlayed } from 'lib/spotify' ;
import { normalizeRecentlyPlayed } from 'lib/utils/normalizers' ;
export default async function handler ( req, res ) {
const response = await getRecentlyPlayed ( ) . catch ( err => {
return res
. status ( 200 )
. json err
The getRecentlyPlayed()
is a simple method that takes the access token and calls the Spotify API.
export const getRecentlyPlayed = async ( ) => {
const limit = config. munber ;
const before = new Date ( ) . getTime ( ) ;
const params = querystring. stringify ( { limit, before } ) ;
const recentlyPlayedEndpoint = ` https://api.spotify.com/v1/me/player/recently-played? ${ params}
Now the API is ready to be consumed by the client. To do that I've create a simple fetcher
to use via server-side.
export async function recentlyPlayedFetcher ( ) {
const recentlyPlayedResponse = await fetch ( ` ${ config. baseUrl } /api/spotify/recently-played ` ) ;
const recentlyPlayed = await recentlyPlayedResponse. json ( ) ;
return recentlyPlayed;
}
I should to put it all togheter in the dedicated page.
app/api/spotify/recently-played.tsx
import { Footer , Header , RecentlyPlayed , Top } from 'components' ;
import { NextSeo } from 'next-seo' ;
import { recentlyPlayedFetcher } from 'app/api/spotify/recently-played' ;
export async function getServerSideProps ( { res } ) {
res. setHeader ( 'Cache-Control' , 'public, s-maxage=10, stale-while-revalidate=59' ) ;
const recentlyPlayed = await recentlyPlayedFetcher
Yes, this component it'v very simple: just SSR
, caching
, fetching
, parsing
and a little bit of SEO
.
The core is the <RecentlyPlayed>
component. It takes the items
as a prop and renders the carousel of tracks.
I didn't used any external components for the carousel
import s from './recently-played.module.css' ;
import config from 'lib/config' ;
import { ChevronUp , Container , Fade , Title , TrackCard } from 'components' ;
import { useCallback, useRef } from 'react' ;
export default function RecentlyPlayed ( { items } ) {
const trackContainerRef = useRef ( ) ;
The scrollTrackContainer uses useCallback
to prevent re-render of the component. It just checks if the trackContainerRef
is defined and if it is, it calls the scroll (native)
method of the node .
Is you are asking why I called a variable munber
, I'll answer you another time (or article).
<Fade>
component just animates the children following the specified criteria.
Actually, I don't feel like to copy and paste part of my code (that you can check here ).
This is because the Top Artist and Tops Tracks
on Spotify is almost the same as the Recently Played
on Spotify .
So I've created a new api in app/api/spotify/top.jsx , that calls two methods: getTopArtists()
and getTopTracks()
. Those methods resolve Spotify endpoints and returns the data.
I have a two methods to normalize both responses.
In this case I've create a single fetcher
that consumes my API. The json looks something like this:
{
"artists" : [ ] ,
"tracks" : [ ]
}
The page has no particular logic, just render the items and make things beautiful using tailwindcss
classes.
:
[
]
,
href: 'https:
id: '2noRn2Aes5aoNVsU6iWThc',
images: [ Array] ,
name: 'Discovery',
release_date: '2001 -03 -12 ',
release_date_precision: 'day',
total_tracks: 14 ,
type: 'album',
uri: 'spotify: album: 2noRn2Aes5aoNVsU6iWThc'
} ,
artists: [ [ Object] ] ,
available_markets: [
'AD', 'AE', 'AG', 'AL', 'AM', 'AO', 'AR', 'AT', 'AU', 'AZ',
'BA', 'BB', 'BD', 'BE', 'BF', 'BG', 'BH', 'BI', 'BJ', 'BN',
'BO', 'BR', 'BS', 'BT', 'BW', 'BY', 'BZ', 'CA', 'CD', 'CG',
'CH', 'CI', 'CL', 'CM', 'CO', 'CR', 'CV', 'CW', 'CY', 'CZ',
'DE', 'DJ', 'DK', 'DM', 'DO', 'DZ', 'EC', 'EE', 'EG', 'ES',
'FI', 'FJ', 'FM', 'FR', 'GA', 'GB', 'GD', 'GE', 'GH', 'GM',
'GN', 'GQ', 'GR', 'GT', 'GW', 'GY', 'HK', 'HN', 'HR', 'HT',
'HU', 'ID', 'IE', 'IL', 'IN', 'IQ', 'IS', 'IT', 'JM', 'JO',
'JP', 'KE', 'KG', 'KH', 'KI', 'KM', 'KN', 'KR', 'KW', 'KZ',
'LA', 'LB', 'LC', 'LI', 'LK', 'LR', 'LS', 'LT', 'LU', 'LV',
... 83 more items
] ,
disc_number: 1 ,
duration_ms: 320357 ,
explicit: false ,
external_ids: { isrc: 'GBDUW0000053' } ,
external_urls: {
spotify: 'https:
} ,
href: 'https:
id: '0DiWol3AO6WpXZgp0goxAV',
is_local: false ,
name: 'One More Time',
popularity: 77 ,
preview_url: 'https:
track_number: 1 ,
type: 'track',
uri: 'spotify: track: 0DiWol3AO6WpXZgp0goxAV'
} ,
currently_playing_type: 'track',
actions: { disallows: { resuming: true , skipping_prev: true } } ,
is_playing: true
}
{
error
:
'Spotify not available'
}
)
;
}
if ( response. status === 204 || response. status > 400 ) {
return res. status ( 200 ) . json ( { is_playing : false } ) ;
}
const data = await response. json ( ) ;
return res. status ( 200 ) . json ( normalizeCurrentlyListening ( data) ) ;
}
Authorization : ` Bearer ${ accessToken} `
}
} ) ;
} ;
thumbnail
:
.
album
?.
[
0
]
?.
,
url : item. external_urls ?. spotify,
progress : progress_ms,
duration : item. duration_ms
} ) ;
)
. then ( response => response. json ( ) )
. then ( setSpotifyListening) ;
useSWR ( '/api/spotify/currently-listening' , fetcher, {
refreshInterval : 10 * 1000
} ) ;
return (
< >
< div className= { s. root } >
< Player / >
< / div>
< / >
) ;
}
config
from
'lib/config'
;
const PlayerAnimation = ( ) => {
return < Lottie loop animationData= { PlayerJson } play style= { { width : '1rem' , height : '1rem' } } / > ;
} ;
const Player = ( ) => {
const { listening } = useUI ( ) ;
const url = listening && listening. isPlaying ? listening. url : ` ${ config. baseUrl } /spotify ` ;
const progress = useMemo (
( ) => listening && ( listening. progress / listening. duration ) * 100 ,
[ listening]
) ;
return (
< >
< div className= { s. root } >
< div className= { s. inner } >
< Link
href= { url}
passHref
target= { listening?. isPlaying ? '_blank' : '_self' }
aria- label= "Mateo Nunez on Spotify"
rel= "noopener noreferer noreferrer"
title= "Mateo Nunez on Spotify"
href= { url} >
{ listening?. isPlaying ? (
< div className= "w-auto h-auto" >
{ }
< img width= "40" height= "40" src= { listening?. thumbnail} alt= { listening?. album} / >
< / div>
) : (
< Spotify className= "w-10 h-10" color= { '#1ED760' } / >
) }
< / Link >
< div className= { s. details } >
< div className= "flex flex-row items-center justify-between" >
< div className= "flex flex-col" >
< p className= { s. title } >
{ listening?. isPlaying ? listening. title : 'Not Listening' }
< / p>
< p className= { s. artist } > { listening?. isPlaying ? listening. artist : 'Spotify' } < / p>
< / div>
< div className= "flex flex-row" >
< Link
href= "/spotify"
passHref
target= { listening?. isPlaying ? '_blank' : '_self' }
aria- label= "Mateo Nunez on Spotify"
rel= "noopener noreferer noreferrer"
title= "Mateo Nunez on Spotify" >
< ChevronUp className= "w-4 h-4 rotate-90" / >
< / Link >
< / div>
< / div>
{ listening?. isPlaying && (
< >
< div className= { s. playingContainer } >
< div className= { s. progress } >
< div className= { s. listened } style= { { width : ` ${ progress} % ` } } / >
< / div>
< div className= { s. animation } >
< PlayerAnimation / >
< / div>
< / div>
< / >
) }
< / div>
< / div>
< / div>
< / >
) ;
} ;
export default React . memo ( Player ) ;
(
{
recently_played
:
false
,
message
:
'Are you connected?'
,
extra
:
}
)
;
} ) ;
if ( ! response) {
return res. status ( 500 ) . json ( { error : 'Spotify not available' } ) ;
}
if ( response. status === 204 || response. status > 400 ) {
return res. status ( 200 ) . json ( { recently_played : false } ) ;
}
const { items = [ ] } = await response. json ( ) ;
const data = items. map ( normalizeRecentlyPlayed) . sort ( ( a, b ) => b. played_at - a. played_at ) ;
return res. status ( 200 ) . json ( data) ;
}
`
;
const { access_token : accessToken } = await getAccessToken ( ) ;
if ( ! accessToken) {
return ;
}
return fetch ( recentlyPlayedEndpoint, {
headers : {
Authorization : ` Bearer ${ accessToken} `
}
} ) ;
} ;
(
)
;
return {
props : {
recentlyPlayed
}
} ;
}
export default function SpotifyPage ( { recentlyPlayed } ) {
return (
< >
< NextSeo
title= "I show you what I 🎧"
description= "I ❤️ the music and you should know it."
openGraph= { {
title : "Mateo's activity on Spotify"
} }
/ >
{ }
< RecentlyPlayed items= { recentlyPlayed} / >
< / >
) ;
}
const scrollTrackContainer = useCallback (
direction => {
const { current } = trackContainerRef;
if ( current) {
current. scroll ( {
left :
direction === 'left'
? current. scrollLeft - current. clientWidth - config. munber
: direction === 'right'
? current. scrollLeft + current. clientWidth - config. munber
: 0 ,
behavior : 'smooth'
} ) ;
}
} ,
[ trackContainerRef. current ]
) ;
return (
< >
< Container clean>
< Fade >
< Title > Recently Played < / Title >
< / Fade >
{ items. length > 0 && (
< div className= { s. root } >
< button
className= { s. navigator }
onClick= { ( ) => {
scrollTrackContainer ( 'left' ) ;
} }
onTouchStart= { ( ) => {
scrollTrackContainer ( 'left' ) ;
} }
aria- label= "Less Tracks" >
< ChevronUp className= "w-6 h-6 font-black transition duration-500 transform -rotate-90" / >
< / button>
< div className= { s[ 'track-container' ] } ref= { trackContainerRef} >
{ items. map ( ( item, key ) => (
< Fade key= { ` ${ item. id } - ${ key} ` } delay= { key + config. munber / 100 } clean>
< TrackCard item= { item} delay= { key + config. munber / 100 } / >
< / Fade >
) ) }
< / div>
< button
className= { s. navigator }
onClick= { ( ) => {
scrollTrackContainer ( 'right' ) ;
} }
onTouchStart= { ( ) => {
scrollTrackContainer ( 'right' ) ;
} }
aria- label= "More Tracks" >
< ChevronUp className= "w-6 h-6 transition duration-500 transform rotate-90 hover:scale-110" / >
< / button>
< / div>
) }
< / Container >
< / >
) ;
}
How to consume Spotify APIs using Next.js and JavaScript | Mateo Nunez