Now that we have a high-level understanding of what OAuth is for and how it works, let's take a closer look at how OAuth works with the Spotify API.
Spotify's Authorization Flows#
According to Spotify's Authorization Guide, there are four possible flows for obtaining app authorization:
Authorization Code Flow
Authorization Code Flow With Proof Key for Code Exchange (PKCE)
Implicit Grant Flow
Client Credentials Flow
Each of these flows provides a slightly different level of authorization due to the way it is granted. For example, the Implicit Grant Flow can be implemented entirely client-side (no server), but it does not provide a refresh token. The Client Credentials Flow is used for server-to-server authentication, but authorization does not grant permission to access user resources. They all follow the OAuth flow we learned in the last lesson, but each has its own variation.
Out of all four of these flows, the Authorization Code Flow is the only one that lets the client access user resources, requires a server-side secret key (an extra layer of security), and provides an access token that can be refreshed. The ability to refresh an access token is a big advantage — users of our app will only need to grant permission once.
Below, we'll be going step-by-step through how to build out this flow with Node and Express. Note that we'll be heavily referring to Spotify's official documentation of the Authorization Code Flow. We'll also be pulling from the Web API Auth Examples, which is referred to in Spotify's official web API tutorial.
Set up environment variables#
Before making any requests to Spotify, we first need to make sure we have our app's client ID and client secret from the Spotify Developer Dashboard.

(Client secret is obfuscated in the screenshot above, since it should stay a secret.)
Once we've located these two values, we'll create a .env
file at the root of our project and add the CLIENT_ID
and CLIENT_SECRET
variables. (Make sure to replace the placeholder values with your own client ID and secret.)
1
CLIENT_ID=XXX
2
CLIENT_SECRET=XXX
We'll be using this .env
file to store sensitive information and other environment variables for our app. In case this code ends up in a place where others can see it, such as a public GitHub repository, we want to make sure those values are kept private.
To ensure our .env
is kept private, let's add it to our .gitignore
file.
1
.DS_Store
2
node_modules
3
.env
Finally, we'll also create a .env.example
file, which will not be gitignored.
1
CLIENT_ID=XXX
2
CLIENT_SECRET=XXX
Since your .env
file should be kept secret (and not checked into source control) it's good practice to add an example .env
file to your codebase for others (or future you) to reference.
Install and use dotenv#
Now that we've stored our app's client ID and secret in a .env
file, we need a way to make our code aware of them. To do that, we'll be using an npm module called dotenv, which lets us load environment variables from a .env
file into process.env
, an object containing the user environment.
In our terminal, let's install the dotenv
module.
1
npm install dotenv
Then, at the very top of our index.js
file, add the following line.
1
require('dotenv').config();
To make sure it's working, we can console.log()
the value of process.env.CLIENT_ID
.
1
require('dotenv').config();
2
3
console.log(process.env.CLIENT_ID);
Now, when we run npm start
in our terminal, we should see the CLIENT_ID
value we have stored in our .env
.

Since Node.js is server-side, console.log()
shows up in the terminal, not the browser console.
Set up the redirect URI#
The last thing we need to do to prepare our environment is set up our redirect URI. The redirect URI is a route of our app that we want the Spotify Accounts Service to redirect the user to once they've authorized our app (i.e. successfully logged into Spotify).
In our case, the redirect URI will be the /callback
route (http://localhost:8888/callback). We'll set up this route handler later.
In the Spotify Developer Dashboard, click the Edit Settings
button and add http://localhost:8888/callback
as a Redirect URI
.

Once the redirect URI has been added, make sure to scroll down and hit the Save
button.

Then, in our .env
and .env.example
files, we'll add that redirect URI as an environment variable.
1
REDIRECT_URI=http://localhost:8888/callback
Now that we have our CLIENT_ID
, CLIENT_SECRET
, and REDIRECT_URI
environment variables all set up, let's store those as constants at the top of our index.js
for convenience.
1
const CLIENT_ID = process.env.CLIENT_ID;
2
const CLIENT_SECRET = process.env.CLIENT_SECRET;
3
const REDIRECT_URI = process.env.REDIRECT_URI;
Sweet! Now our development environment is all ready to go.
Step 1: Request authorization from Spotify#
The first step in Spotify's Authorization Code Flow is having our app request authorization from the Spotify Accounts Service. In code terms, this means sending a GET
request to the Spotify Accounts Service /authorize
endpoint.
1
GET https://accounts.spotify.com/authorize
To trigger that HTTP request, we can set up a route in our Express app that can eventually be hooked up to a button on the front end with an anchor link like so:
1
<a href="http://localhost:8888/login">Log in to Spotify</a>
Set up the /login
route handler#
Let's set up a route handler for the /login
endpoint in our index.js
1
app.get('/login', (req, res) => {
2
res.send('Log in to Spotify');
3
});
To make sure the route handler is working, let's visit http://localhost:8888/login and make sure we see the text Log in to Spotify
.

Next, we want to set up our /login
route to hit the Spotify Accounts Service https://accounts.spotify.com/authorize
endpoint. To do that, we'll replace our res.send()
with a res.redirect()
.
1
app.get('/login', (req, res) => {
2
res.redirect('https://accounts.spotify.com/authorize');
3
});
Now, when we refresh the /login
page, we'll be automatically redirected to the Spotify Accounts Service URL (https://accounts.spotify.com/authorize). However, there will be an error saying that there is a missing required parameter.

This is because the /authorize
endpoint has required query parameters which we failed to include. (Check out all required and optional query parameters for the endpoint in the documentation.)
The three required query parameters for the /authorize
endpoint are: client_id
, response_type
, and redirect_uri
. There are also other optional query params for things such as authorization scopes and security that we'll eventually include.
Let's modify our res.redirect()
to include the required query params with template strings:
1
app.get('/login', (req, res) => {
2
res.redirect(`https://accounts.spotify.com/authorize?client_id=${CLIENT_ID}&response_type=code&redirect_uri=${REDIRECT_URI}`);
3
});
If we visit the http://localhost:8888/login route again, we'll now be redirected to the Spotify Accounts Service login page instead of seeing an error. Progress!

In the general OAuth flow, this is step 2 — Spotify authorizes access to client.
Once we log in with our username and password, we'll be redirected to a consent page where we can agree to let Spotify access our data.

In the general OAuth flow, this is step 3 — User grants app access their Spotify data
Once we hit Agree
, Spotify will then redirect us to the REDIRECT_URI
(http://localhost:8888/callback) we provided in the query params of the /authorize
request.

Unfortunately, since our app doesn't have a /callback
route handler yet, we'll see an error. Regardless, we can tell our request was successful because there is a code
query param in our /callback
route.
The value of the code
query param is an authorization code that we will be able to exchange for an access token. We're getting closer to our goal of getting an access token from Spotify!
Refactor with the querystring
module#
Before we exchange the authorization code for an access token, let's refactor our existing code to make handling query params easier and less error-prone.
There's a built-in Node module called querystring
that lets us parse and stringify query strings. Let's import it at the top of our index.js
:
1
const querystring = require('querystring');
querystring.stringify()
takes an object with keys and values and serializes them into a query string. No more having to keep track of ampersands and equal signs!
1
app.get('/login', (req, res) => {
2
const queryParams = querystring.stringify({
3
client_id: CLIENT_ID,
4
response_type: 'code',
5
redirect_uri: REDIRECT_URI,
6
});
7
8
res.redirect(`https://accounts.spotify.com/authorize?${queryParams}`);
9
});
In the snippet above, queryParams
evaluates to something like client_id=abc123&response_type=code&redirect_uri=http://localhost:8888/callback
. Then, all we have to do is append ?${queryParams}
to the end of the /authorize
endpoint template string.
Add state
and scope
query params#
Now that we have an easier way of handling query params in our HTTP requests, let's add the optional query params on the /authorize
endpoint that we didn't include before.
1
/**
2
* Generates a random string containing numbers and letters
3
* @param {number} length The length of the string
4
* @return {string} The generated string
5
*/
6
const generateRandomString = length => {
7
let text = '';
8
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
9
for (let i = 0; i < length; i++) {
10
text += possible.charAt(Math.floor(Math.random() * possible.length));
11
}
12
return text;
13
};
14
15
16
const stateKey = 'spotify_auth_state';
17
18
app.get('/login', (req, res) => {
19
const state = generateRandomString(16);
20
res.cookie(stateKey, state);
21
22
const scope = 'user-read-private user-read-email';
23
24
const queryParams = querystring.stringify({
25
client_id: CLIENT_ID,
26
response_type: 'code',
27
redirect_uri: REDIRECT_URI,
28
state: state,
29
scope: scope,
30
});
31
32
res.redirect(`https://accounts.spotify.com/authorize?${queryParams}`);
33
});
In the snippet above, we first update the /login
handler to use the generateRandomString()
utility function to generate a random string for the state
query param and cookie. The state
query param is kind of a security measure — it protects against attacks such as cross-site request forgery.
We also add the scope
query param, which is a space-separated list of Spotify's pre-defined authorization scopes. Let's pass two scopes for now: user-read-private
and user-read-email
. These scopes will let us access details about the currently logged-in user's account and their email.
Now, when we visit the /login
route and log into Spotify, we should see a consent page with updated scope details:

Great! Now let's use the authorization code we received from Spotify's /authorize
endpoint to request an access token.
Step 2: Use authorization code to request access token#
Currently, once the user logs into Spotify and gets redirected back to our app, they hit an error page. This is because we haven't set up our /callback
route handler yet.

Set up the /callback
route handler#
Let's stub it out:
1
app.get('/callback', (req, res) => {
2
res.send('Callback');
3
});
To exchange the authorization code for an access token, we need this route handler to send a POST
request to the Spotify Accounts Service /api/token
endpoint.
1
POST https://accounts.spotify.com/api/token
Similar to the /authorize
endpoint, the /api/token
endpoint has required body parameters. The three params required for the /api/token
endpoint are grant_type
, code
, and redirect_uri
. When sent along in the body of the POST
request, they need to be encoded in the application/x-www-form-urlencoded
format.
grant_type
:authorization_code
code
: The authorization code (thecode
query param on the/callback
URL)redirect_uri
: TheREDIRECT_URI
(http://localhost:8888/callback)
The token endpoint also has a required Authorization
header, which needs to be a base 64 encoded string in this format: Authorization: Basic <base 64 encoded client_id:client_secret>
.
Set up the POST
request with Axios#
Now, let's create our POST
request in our /callback
route handler to send the authorization code back to the Spotify server.
Although it's possible to send a POST
request with Node's built-in modules, it can get pretty verbose and clunky. A popular abstraction we can use instead is the Axios library, which provides a simpler API. Other than being easy to use, Axios also works both client-side (in the browser) and server-side (in our Express app).
Let's install axios
as a dependency.
1
npm install axios
Then require
it at the top of our index.js
file.
1
const axios = require('axios');
Now let's set up the POST
request to https://accounts.spotify.com/api/token
with the axios()
method in our /callback
route handler.
1
app.get('/callback', (req, res) => {
2
const code = req.query.code || null;
3
4
axios({
5
method: 'post',
6
url: 'https://accounts.spotify.com/api/token',
7
data: querystring.stringify({
8
grant_type: 'authorization_code',
9
code: code,
10
redirect_uri: REDIRECT_URI
11
}),
12
headers: {
13
'content-type': 'application/x-www-form-urlencoded',
14
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
15
},
16
})
17
.then(response => {
18
if (response.status === 200) {
19
res.send(`<pre>${JSON.stringify(response.data, null, 2)}</pre>`);
20
} else {
21
res.send(response);
22
}
23
})
24
.catch(error => {
25
res.send(error);
26
});
27
});
There's a bunch of things happening in this chunk of code, so let's break it down a bit.
First, we store the value of our authorization code which we got from the code
query param in the code
variable.
1
const code = req.query.code || null;
In Express, req.query
is an object containing a property for each query string parameter a route. For example, if the route was /callback?code=abc123&state=xyz789
, req.query.code
would be abc123
and req.query.state
would be xyz789
. If for some reason the route doesn't have a code
query param, we set null
as a fallback.
Next, we set up the POST
request to https://accounts.spotify.com/api/token
by passing a config object to the axios()
method, which will send the request when invoked.
1
axios({
2
method: 'post',
3
url: 'https://accounts.spotify.com/api/token',
4
data: querystring.stringify({
5
grant_type: 'authorization_code',
6
code: code,
7
redirect_uri: REDIRECT_URI
8
}),
9
headers: {
10
'content-type': 'application/x-www-form-urlencoded',
11
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
12
},
13
})
In the data
object, we use querystring.stringify()
to format the three required body params. The code
variable we declared above is passed as one of the params here.
We also set two header params in the headers
object: a content-type
header and an Authorization
header.
Then, we chain .then()
and .catch()
callback functions to handle resolving the promise the axios()
method returns. If you're unfamiliar with promises, learn more about them in the MDN docs.
1
.then(response => {
2
if (response.status === 200) {
3
res.send(`<pre>${JSON.stringify(response.data, null, 2)}</pre>`);
4
} else {
5
res.send(response);
6
}
7
})
8
.catch(error => {
9
res.send(error);
10
});
If our request is successful (i.e. returns with a 200
status code), the .then()
callback will be invoked, where we return the stringified response.data
object from the Axios response. (It's important to note here that Axios stores the data returned by requests in the data
property of the response
object, not the response
object itself.)
On the other hand, if our request fails, the error will be caught in the .catch()
callback, in which case we just return the error message.
Our use of the JSON.stringify() method (<pre>${JSON.stringify(response.data, null, 2)}</pre>
) is a just a handy way to format JSON nicely in the browser. Simply returning response.data
would also work, but the JSON displayed in the browser wouldn't be formatted.
Once we visit the http://localhost:8888/login endpoint again, we should see the JSON data returned by Spotify's /api/token
endpoint, which includes the special access_token
we've been waiting for! 🎉

In the general OAuth flow, this is step 4 — client receives access token from Spotify.
Step 3: Use access token to request data from the Spotify API#
Now that we finally have an access token, we can test out requesting some user data from the Spotify API.
Let's modify the .then()
callback function to send a GET
request to the https://api.spotify.com/v1/me endpoint, which will return detailed profile information about the current user.
In the general OAuth flow, this is step 5 — client uses access token to request data from Spotify.
1
.then(response => {
2
if (response.status === 200) {
3
4
const { access_token, token_type } = response.data;
5
6
axios.get('https://api.spotify.com/v1/me', {
7
headers: {
8
Authorization: `${token_type} ${access_token}`
9
}
10
})
11
.then(response => {
12
res.send(`<pre>${JSON.stringify(response.data, null, 2)}</pre>`);
13
})
14
.catch(error => {
15
res.send(error);
16
});
17
18
} else {
19
res.send(response);
20
}
21
})
22
.catch(error => {
23
res.send(error);
24
});
Notice that we use destructuring to store the access_token
and token_type
as variables to pass into the Authorization
header.
Now, when we visit our /login
route again, our /callback
route will display the user profile JSON data returned from Spotify's /me
endpoint.

Sweet! This opens up a bunch of new doors for us to interact with the Spotify API in interesting ways. We'll get to this soon!
Bonus step: Refresh access token#
Before we move on to making more requests to the Spotify API, there's one last thing we need to do to cover our bases with the authorization code flow.
If we take another look at the data returned by Spotify's /api/token
endpoint, we'll see that in addition to the access_token
, there are also a token_type
, scope
, expires_in
, and refresh_token
values.
1
{
2
"access_token": "NgCXRK...MzYjw",
3
"token_type": "Bearer",
4
"scope": "user-read-private user-read-email",
5
"expires_in": 3600,
6
"refresh_token": "NgAagA...Um_SHo"
7
}
The expires_in
value is the number of seconds that the access_token
is valid. This means after 3600 seconds, or 60 minutes, our access_token
will expire.
Once the token is expired, there are two things that could happen: 1) we force the user to log in again, or 2) we use the refresh_token
to retrieve another access token behind the scenes, not requiring the user log in again. The better user experience is definitely the latter, so let's make sure our app has a way to handle that.
Set up the /refresh_token
route handler#
Let's set up a route handler to handle requesting a new access token with our refresh token.
1
app.get('/refresh_token', (req, res) => {
2
const { refresh_token } = req.query;
3
4
axios({
5
method: 'post',
6
url: 'https://accounts.spotify.com/api/token',
7
data: querystring.stringify({
8
grant_type: 'refresh_token',
9
refresh_token: refresh_token
10
}),
11
headers: {
12
'content-type': 'application/x-www-form-urlencoded',
13
Authorization: `Basic ${new Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64')}`,
14
},
15
})
16
.then(response => {
17
res.send(response.data);
18
})
19
.catch(error => {
20
res.send(error);
21
});
22
});
This route handler is almost the same as the /callback
handler, except for a few small differences. The grant_type
is refresh_token
instead of authorization_code
, and we're sending along a refresh_token
instead of an authorization code
.
To test the route handler, we'll have to go through the login flow again. For testing purposes, let's replace the GET
request we made to the https://api.spotify.com/v1/me
endpoint in the /callback
route handler with a GET
request to our local /refresh_token
endpoint (http://localhost:8888/refresh_token).
1
const { refresh_token } = response.data;
2
3
axios.get(`http://localhost:8888/refresh_token?refresh_token=${refresh_token}`)
4
.then(response => {
5
res.send(`<pre>${JSON.stringify(response.data, null, 2)}</pre>`);
6
})
7
.catch(error => {
8
res.send(error);
9
});
Now if we visit our /login
endpoint again, we'll see that the JSON data we receive from Spotify includes a new access_token
.

Awesome! That's all we need to have set up for now. In the next module, we'll learn how to use the access and refresh tokens on the front end and make requests to the Spotify API in a React app.
Lesson recap#
That was a ton to take in for one lesson, so let's sum up what we've learned. From the perspective of our app, there were three main steps in the Spotify authorization code OAuth flow:
Request authorization from Spotify (using the
/login
route handler)Use the authorization code to request an access token from Spotify (using the
/callback
route handler)Use the access token to request data from the Spotify API
There was also a bonus step, where we used the refresh token to request another access token from Spotify in the case that our access token expires.
We accomplished implementing this flow by setting up multiple route handlers in our Express app to handle sending requests to the Spotify Accounts Service. We also employed the help of the Axios library to easily construct HTTP requests.
Here's a diagram of the authorization code flow to help you visualize the back and forth between our app and Spotify:

Sources#
https://developer.spotify.com/documentation/general/guides/authorization-guide/#authorization-flows
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Using_promises
Learning Checkpoint#
Awesome job making it to the end of Module 2! That was a ton of information to take in. Feel free to go back over these last few lessons to make sure everything makes sense in your head.
In this module, we've covered...
How to set up an app in the Spotify developer dashboard
What OAuth is and how it works
The basics of Spotify’s different authorization flows
How to implement Spotify's Authorization Code Flow with Express route handlers
(Video) How to Authenticate and use Spotify Web API
In the next module, we'll be setting up a React app for our front end. We'll learn how to to pass the access token we've acquired from the server to the front end so we can fetch data from the Spotify API through our React app.