Getting Spotify Now Playing
I like having a Now Playing section on my site. Here’s a quick reference on how to set it up so I can always come back to it.
We need an API route that returns the track currently playing on Spotify.
Authorisation
- Create an app in the Spotify Developer portal, use
http://127.0.0.1:4321for the callback URL and select Web API. (Note:4321is Astro’s default port, change this if your development setup uses a different port like3000). Copy theclient_id. - Make a GET request to the following URL (replace
YOUR_CLIENT_IDand adjust the port if necessary):
https://accounts.spotify.com/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=http://127.0.0.1:4321&scope=user-read-currently-playing
- Authorise the application.
- It will redirect you to your local app with a long URL. Copy the
codeparameter from the URL. - Create a simple script to get the refresh token. For example,
encode.py:
import base64
import json
import urllib.request
import urllib.parse
AUTH_CODE = "YOUR_AUTHORISATION_CODE_FROM_STEP_4"
REDIRECT_URI = "http://127.0.0.1:4321"
client_id = 'YOUR_CLIENT_ID'
client_secret = 'YOUR_CLIENT_SECRET'
base64_credentials = base64.b64encode(f"{client_id}:{client_secret}".encode()).decode()
def get_spotify_token():
params = {
"grant_type": "authorization_code",
"code": AUTH_CODE,
"redirect_uri": REDIRECT_URI
}
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {base64_credentials}"
}
data = urllib.parse.urlencode(params).encode()
req = urllib.request.Request(
"https://accounts.spotify.com/api/token",
data=data,
headers=headers,
method="POST"
)
with urllib.request.urlopen(req) as response:
resp_data = response.read()
print(json.loads(resp_data.decode()))
get_spotify_token()
The response will print out your tokens, including the refresh_token. Save this!
Serverless API Route
Add your credentials to your .env file:
CLIENT_ID=YOUR_CLIENT_ID
CLIENT_SECRET=YOUR_CLIENT_SECRET
REFRESH_TOKEN=YOUR_REFRESH_TOKEN
Since I didn’t want to turn my static site into a server-side rendered (SSR) application, I opted to use a Vercel serverless function to handle the token exchange securely. (If this were a Next.js project, I’d just drop this into an /api route).
I like to keep my Spotify logic in a helper file, for example src/lib/spotify.ts:
// src/lib/spotify.ts
import querystring from 'querystring';
const clientID = import.meta.env.CLIENT_ID;
const clientSecret = import.meta.env.CLIENT_SECRET;
const refreshToken = import.meta.env.REFRESH_TOKEN;
const TOKEN_URL = `https://accounts.spotify.com/api/token`;
const basicAuth = Buffer.from(`${clientID}:${clientSecret}`).toString('base64');
const getAccessToken = async () => {
const response = await fetch(TOKEN_URL, {
method: 'POST',
headers: {
Authorization: `Basic ${basicAuth}`,
'Content-Type': 'application/x-www-form-urlencoded'
},
body: querystring.stringify({
grant_type: 'refresh_token',
refresh_token: refreshToken
})
});
return response.json();
};
const NOW_PLAYING_URL = `https://api.spotify.com/v1/me/player/currently-playing`;
export const getNowPlaying = async () => {
const { access_token } = await getAccessToken();
return fetch(NOW_PLAYING_URL, {
headers: {
Authorization: `Bearer ${access_token}`
}
});
};
And expose it via the serverless function endpoint api/spotify.ts:
// api/spotify.ts
import type { VercelRequest, VercelResponse } from '@vercel/node';
import path from 'path';
// Vercel's serverless functions can't import files above the /api directory directly with relative paths like '../src/lib/spotify'.
// Instead, use an absolute path using the process.cwd(), e.g.,
// https://vercel.com/kb/guide/how-can-i-use-files-in-serverless-functions
// Dynamically import the file using an absolute path (ESM compatible)
const spotifyLibPath = path.join(process.cwd(), 'src', 'lib', 'spotify.js');
export interface SpotifyPlayerResponse {
is_playing: boolean;
item: {
name: string;
artists: { name: string }[];
album: {
name: string;
images: { url: string }[];
};
external_urls: {
spotify: string;
};
duration_ms: number;
};
progress_ms: number;
}
export default async function handler(req: VercelRequest, res: VercelResponse) {
// Use dynamic import to load the module in ESM
const { getNowPlaying } = await import(spotifyLibPath);
const response = await getNowPlaying();
if (response.status === 204 || response.status > 400) {
res.setHeader('Content-Type', 'application/json');
res.status(200).send(JSON.stringify({ isPlaying: false }));
return;
}
const player = (await response.json()) as SpotifyPlayerResponse;
const isPlaying = player.is_playing;
const title = player.item.name;
const artist = player.item.artists.map((_artist) => _artist.name).join(', ');
const album = player.item.album.name;
const albumImageUrl = player.item.album.images[0].url;
const songUrl = player.item.external_urls.spotify;
const duration = player.item.duration_ms;
const progress = player.progress_ms;
res.setHeader('Content-Type', 'application/json');
res.status(200).send(
JSON.stringify({
isPlaying,
title,
artist,
album,
albumImageUrl,
songUrl,
duration,
progress,
})
);
}
Wrapping Up
With this API route ready, we can fetch the isPlaying status and track info from any frontend component. Keeping it serverless means my site stays fast and static, but I still get live data.
(Reference: Tommy Palmer’s original guide)