Skip to content

Display your Spotify profile data in a web app

This guide creates a simple client-side application that uses the Spotify Web API to get user profile data. We'll show both TypeScript and JavaScript code snippets, make sure to use the code that is correct for your application.

External applications can use the Spotify Web API to retrieve Spotify content, such as song data, album data and playlists. However, in order to access user-related data with the Spotify Web API, an application must be authorized by the user to access that particular information.

Prerequisites

To work through this guide you'll need:

Set up your account

Login to the Spotify Developer Dashboard. If necessary, accept the latest Developer Terms of Service to complete your account set up.

Creating a Spotify app

We will need to register a new app to generate valid credentials - we'll use these credentials later to perform API calls. Follow the apps guide to learn how to create an app and generate the necessary credentials.

Once you've created your app, make a note of your client_id.

Creating a new project

This app uses Vite as a development server. We'll scaffold a new project with the Vite create command and use their default template to give us a basic app:

TypeScript
JavaScript

_10
npm create vite@latest spotify-profile-demo -- --template vanilla-ts

Select y when it prompts you to install Vite.

Change directory to the new app directory that Vite just created and start the development server:


_10
cd spotify-profile-demo
_10
npm install
_10
npm run dev

The default Vite template creates some files that we won't need for this demo, so you can delete all of the files in ./src/ and ./public/

Creating the user interface

This demo is going to be a single page application that runs entirely in the browser. We're going to replace the provided index.html file with a simple HTML page that constitutes the user interface to display the user's profile data.

Start by deleting the content of the index.html file and replacing it with a html and head tag that references a TypeScript/JavaScript file (src/script.ts, or src/script.js, we'll create this file later).

TypeScript
JavaScript

_16
<!DOCTYPE html>
_16
<html lang="en">
_16
<head>
_16
<meta charset="utf-8">
_16
<title>My Spotify Profile</title>
_16
<script src="src/script.ts" type="module"></script>
_16
</head>
_16
<body>
_16
_16
</body>
_16
</html>
_16
_16
<!-- Note- We're referring directly to the TypeScript file,
_16
and we're using the `type="module"` attribute.
_16
Vite will transpile our TypeScript to JavaScript
_16
so that it can run in the browser. -->

Inside the body, we'll add some markup to display the profile data:


_13
<h1>Display your Spotify profile data</h1>
_13
_13
<section id="profile">
_13
<h2>Logged in as <span id="displayName"></span></h2>
_13
<span id="avatar"></span>
_13
<ul>
_13
<li>User ID: <span id="id"></span></li>
_13
<li>Email: <span id="email"></span></li>
_13
<li>Spotify URI: <a id="uri" href="#"></a></li>
_13
<li>Link: <a id="url" href="#"></a></li>
_13
<li>Profile Image: <span id="imgUrl"></span></li>
_13
</ul>
_13
</section>

Some elements in this block have id attributes. We'll use these to replace the element's text with the data we fetch from the Web API.

Calling the Web API

We're going to use the Web API to get the user's profile data. We'll use the authorization code flow with PKCE to get an access token, and then use that token to call the API.

How it works

  • When the page loads, we'll check if there is a code in the callback query string
  • If we don't have a code, we'll redirect the user to the Spotify authorization page.
  • Once the user authorizes the application, Spotify will redirect the user back to our application, and we'll read the code from the query string.
  • We will use the code to request an access token from the Spotify token API
  • We'll use the access token to call the Web API to get the user's profile data.
  • We'll populate the user interface with the user's profile data.

Create a src/script.ts or src/script.js file and add the following code:

TypeScript
JavaScript

_26
const clientId = "your-client-id-here"; // Replace with your client id
_26
const code = undefined;
_26
_26
if (!code) {
_26
redirectToAuthCodeFlow(clientId);
_26
} else {
_26
const accessToken = await getAccessToken(clientId, code);
_26
const profile = await fetchProfile(accessToken);
_26
populateUI(profile);
_26
}
_26
_26
async function redirectToAuthCodeFlow(clientId: string) {
_26
// TODO: Redirect to Spotify authorization page
_26
}
_26
_26
async function getAccessToken(clientId: string, code: string) {
_26
// TODO: Get access token for code
_26
}
_26
_26
async function fetchProfile(token: string): Promise<any> {
_26
// TODO: Call Web API
_26
}
_26
_26
function populateUI(profile: any) {
_26
// TODO: Update UI with profile data
_26
}

This is the outline of our application.

On the first line there is a clientId variable - you'll need to set this variable to the client_id of the Spotify app you created earlier.

The code now needs to be updated to redirect the user to the Spotify authorization page. To do this, let's write the redirectToAuthCodeFlow function:

TypeScript
JavaScript

_35
export async function redirectToAuthCodeFlow(clientId: string) {
_35
const verifier = generateCodeVerifier(128);
_35
const challenge = await generateCodeChallenge(verifier);
_35
_35
localStorage.setItem("verifier", verifier);
_35
_35
const params = new URLSearchParams();
_35
params.append("client_id", clientId);
_35
params.append("response_type", "code");
_35
params.append("redirect_uri", "http://localhost:5173/callback");
_35
params.append("scope", "user-read-private user-read-email");
_35
params.append("code_challenge_method", "S256");
_35
params.append("code_challenge", challenge);
_35
_35
document.location = `https://accounts.spotify.com/authorize?${params.toString()}`;
_35
}
_35
_35
function generateCodeVerifier(length: number) {
_35
let text = '';
_35
let possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
_35
_35
for (let i = 0; i < length; i++) {
_35
text += possible.charAt(Math.floor(Math.random() * possible.length));
_35
}
_35
return text;
_35
}
_35
_35
async function generateCodeChallenge(codeVerifier: string) {
_35
const data = new TextEncoder().encode(codeVerifier);
_35
const digest = await window.crypto.subtle.digest('SHA-256', data);
_35
return btoa(String.fromCharCode.apply(null, [...new Uint8Array(digest)]))
_35
.replace(/\+/g, '-')
_35
.replace(/\//g, '_')
_35
.replace(/=+$/, '');
_35
}

In this function, a new URLSearchParams object is created, and we add the client_id, response_type, redirect_uri and scope parameters to it. The scope parameter is a list of permissions that we're requesting from the user. In this case, we're requesting the user-read-private and user-read-email scopes - these are the scopes that allow us to fetch the user's profile data.

The redirect_uri parameter is the URL that Spotify will redirect the user back to after they've authorized the application. In this case, we're using a URL that points to our local Vite dev server.

You need to make sure this URL is listed in the Redirect URIs section of your Spotify Application Settings in your Developer Dashboard.

Edit settings to add your Redirect URI to your app

You will also notice that we are generating PKCE verifier and challenge data, we're using this to verify that our request is authentic. We're using local storage to store the verifier data, which works like a password for the token exchange process.

To prevent the user from being stuck in a redirect loop when they authenticate, we need to check if the callback contains a code parameter. To do this, the first three lines of code in the file are modified like this:


_11
const clientId = "your_client_id";
_11
const params = new URLSearchParams(window.location.search);
_11
const code = params.get("code");
_11
_11
if (!code) {
_11
redirectToAuthCodeFlow(clientId);
_11
} else {
_11
const accessToken = await getAccessToken(clientId, code);
_11
const profile = await fetchProfile(accessToken);
_11
populateUI(profile);
_11
}

In order to make sure that the token exchange works, we need to write the getAccessToken function.

TypeScript
JavaScript

_19
export async function getAccessToken(clientId: string, code: string): Promise<string> {
_19
const verifier = localStorage.getItem("verifier");
_19
_19
const params = new URLSearchParams();
_19
params.append("client_id", clientId);
_19
params.append("grant_type", "authorization_code");
_19
params.append("code", code);
_19
params.append("redirect_uri", "http://localhost:5173/callback");
_19
params.append("code_verifier", verifier!);
_19
_19
const result = await fetch("https://accounts.spotify.com/api/token", {
_19
method: "POST",
_19
headers: { "Content-Type": "application/x-www-form-urlencoded" },
_19
body: params
_19
});
_19
_19
const { access_token } = await result.json();
_19
return access_token;
_19
}

In this function, we load the verifier from local storage and using both the code returned from the callback and the verifier to perform a POST to the Spotify token API. The API uses these two values to verify our request and it returns an access token.

Now, if we run npm run dev, and navigate to http://localhost:5173 in a browser, we'll be redirected to the Spotify authorization page. If we authorize the application, we'll be redirected back to our application, but no data will be fetched and displayed.

To fix this, we need to update the fetchProfile function to call the Web API and get the profile data. Update the fetchProfile function:

TypeScript
JavaScript

_10
async function fetchProfile(token: string): Promise<any> {
_10
const result = await fetch("https://api.spotify.com/v1/me", {
_10
method: "GET", headers: { Authorization: `Bearer ${token}` }
_10
});
_10
_10
return await result.json();
_10
}

In this function, a call is made to https://api.spotify.com/v1/me using the browser's Fetch API to get the profile data. The Authorization header is set to Bearer ${token}, where token is the access token that we got from the https://accounts.spotify.com/api/token endpoint.

If we add a console.log statement to the calling code we can see the profile data that is returned from the API in the browser's console:


_10
} else {
_10
const profile = await fetchProfile(token);
_10
console.log(profile); // Profile data logs to console
_10
...
_10
}

Finally, we need to update the populateUI function to display the profile data in the UI. To do this, we'll use the DOM to find our HTML elements and update them with the profile data:

TypeScript
JavaScript

_15
function populateUI(profile: any) {
_15
document.getElementById("displayName")!.innerText = profile.display_name;
_15
if (profile.images[0]) {
_15
const profileImage = new Image(200, 200);
_15
profileImage.src = profile.images[0].url;
_15
document.getElementById("avatar")!.appendChild(profileImage);
_15
}
_15
document.getElementById("id")!.innerText = profile.id;
_15
document.getElementById("email")!.innerText = profile.email;
_15
document.getElementById("uri")!.innerText = profile.uri;
_15
document.getElementById("uri")!.setAttribute("href", profile.external_urls.spotify);
_15
document.getElementById("url")!.innerText = profile.href;
_15
document.getElementById("url")!.setAttribute("href", profile.href);
_15
document.getElementById("imgUrl")!.innerText = profile.images[0]?.url ?? '(no profile image)';
_15
}

You can now run your code by running npm run dev in the terminal and navigating to http://localhost:5173 in your browser.

Your profile data will display as a heading with your name, show your avatar image and then list your profile details

Adding extra type safety for TypeScript developers

At the moment, even though we're using TypeScript, we don't have any type safety around the data being returned from the Web API. To improve this, we can create a UserProfile interface to describes the data that we expect to be returned from the API. Adding an interface will define the shape of the object that we're expecting, this will make using the data type-safe and will allow for type prompts while coding, making a more pleasant developer experience if you extend this project in future.

To do this, create a new file called types.d.ts in the src folder and add the following code:


_23
interface UserProfile {
_23
country: string;
_23
display_name: string;
_23
email: string;
_23
explicit_content: {
_23
filter_enabled: boolean,
_23
filter_locked: boolean
_23
},
_23
external_urls: { spotify: string; };
_23
followers: { href: string; total: number; };
_23
href: string;
_23
id: string;
_23
images: Image[];
_23
product: string;
_23
type: string;
_23
uri: string;
_23
}
_23
_23
interface Image {
_23
url: string;
_23
height: number;
_23
width: number;
_23
}

We can now update our calling code to expect these types:


_10
async function fetchProfile(token: string): Promise<UserProfile> {
_10
// ...
_10
}
_10
_10
function populateUI(profile: UserProfile) {
_10
// ...
_10
}

You can view and fork the final code for this demo on GitHub: Get User Profile Repository.