This is Part 1 of my guide to building an app using the Spotify API and Slack API. This app lets you create Spotify playlists directly from Slack and add any songs to that playlist when it is sent as messages in Slack.
The tech stack:
- Spotify API
- Slack API
- Node.js
- Express
- Redis
Table of Contents
Creating a Simple Server
Create a new project directory and inside it add the following files src/index.js
and package.json
. Install the following dependencies with yarn
yarn add -D nodemon
yarn add axios express redis
We will learn why we need each dependency throughout this guide.
The following code creates a server.
const express = require("express");
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Server started. Listening on http://localhost:${port}`);
});
app.get("/authorized", (req, res) => console.log(req));
express is a popular web framework for Node.js applications. It helps us spin up a simple server and endpoints which listen for incoming requests. In this case we started a local server on port 3000. We set up a GET
route at /authorized
which will be used by the Spotify API in the following section. When Spotify sends us a request at that URL, we will log the request details into the console.
The dev dependency nodemon
is a package that will make development faster by restarting your local development servers whenever it detects any file changes. Run your newly created development server with nodemon
(assuming you put the code in src/index.js
):
nodemon src/index.js
ngrok
ngrok is a tool that enables us to create a secure, public facing URL for our local servers. When interacting with APIs which need to send data to your server, the APIs won’t be able to connect to servers running locally on your own computer. ngrok will tunnel requests to a public URL to your local computer so the APIs can send you data.
Navigate to the directory ngrok was saved to and run it with the following:
./ngrok http 3000
The url that looks like https://randomNumbersAndLetters.ngrok.io
is your new publicly facing URL. Requests sent to this URL will get sent to the server running on http://localhost:3000
. Keep the ngrok server running, we will need the URL when setting up Spotify.
Spotify
Initial Setup
To start building apps with the Spotify API, you will need to create a Spotify account. Use your login to access the Developer Dashboard and follow the instructions to create a new app. Once it’s created find the Client ID
and Client Secret
. We will need these keys to use the API so save them in either the .bashrc
, .zshrc
or .env.development
file.
Creating a Redirect URI
When you authenticate with the Spotify API, Spotify will send a request to whatever server you want. The request will contain information such as access tokens, refresh tokens and permission scopes.
In the Dashboard, click Edit Setting
and locate the field asking for a Redirect URI
. Add the ngrok
URL we created in the previous section with /authorized
appended to the end.
https://123abcd.ngrok.io/authorized
Great! Now Spotify can send us a request when we authenticate with them.
Authentication
Before we can do anything with the API (create playlists, add songs, get artists etc.) we need to authenticate with our Spotify account. For a more in depth look at the flow for authenticating read the Authorization Guide.
The first step is to generate a Spotify authorization URL that we can navigate to in our browsers and sign in.
The URL we need to navigate to is https://accounts.spotify.com/authorize
. But it requires some query parameters. See the function below which generates this URL
const getAuthorizationUrl = () => {
const params = {
client_id: SPOTIFY_CLIENT_ID,
response_type: "code",
redirect_uri: 'https://123abcd.ngrok.io/authorized',
scope: "user-read-private playlist-modify-private playlist-read-collaborative";,
};
const encodedQueryParams = encodeQueryParams(params);
return `https://accounts.spotify.com/authorize?${encodedQueryParams}`;
};
const encodeQueryParams = (obj) => {
let str = [];
for (let key in obj)
if (obj.hasOwnProperty(key)) {
str.push(encodeURIComponent(key) + "=" + encodeURIComponent(obj[key]));
}
return str.join("&");
};
client_id
is the Spotify Client ID from their Developer Dashboardresponse_type
is always set to “code”redirect_uri
is the ngrok Redirect URI we set up earlierscope
is a string with space separated scopes. These are the permissions that the Spotify App will have. A list of all scopes can be found here
The encodeQueryParams
method is a simple helper method used to take the params
object and convert it into URI encoded query parameters. If in the future we need to update the query parameters or add additional ones, all we need to do is edit the params
object instead of trying to modify a complex URL string.
This function should return a url that looks like
https://accounts.spotify.com/authorize?client_id=yourClientId&response_type=code&redirect_uri=https%3A%2F%2F123abcdef.ngrok.io%2Fauthorized&scope=user-read-private%20playlist-modify-private%20playlist-read-collaborative&show_dialog=false
Retrieve Access Token
Navigate to this URL and follow the steps to authorize your Spotify account. Once the authorization is complete, a request will be made to the local server running on port 3000 and the request should be logged to the console. In this request, there is an authorization code under request.query.code
. We need to use this to exchange it for an access token.
To retrieve the access token we need to make a request to the https://accounts.spotify.com/api/token
endpoint. See the function below:
const getAccessToken = (requestCode) => {
return axios({
url: "https://accounts.spotify.com/api/token",
method: "post",
params: {
grant_type: "authorization_code",
code: requestCode,
redirect_uri: "https://123abcd.ngrok.io/authorized",
},
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${encodeAuthorizationToBase64()}`,
},
});
};
Pass in the authorization code that was returned into this getAccessToken
function. The Authorization
header contains a Base64 encoded string comprised of the Spotify Client ID
and Client Secret
, separated by a :
const encodeAuthorizationToBase64 = () => {
const stringToEncode = `${process.env.SPOTIFY_CLIENT_ID}:${process.env.SPOTIFY_CLIENT_SECRET}`;
return Buffer.from(stringToEncode).toString("base64");
};
At this point your code should look something similar to this:
app.get("/authorized", authCallback);
const authCallback = async (req, res) => {
const requestCode = req.query.code;
const response = await getAccessToken(requestCode);
const accessToken = response.data.access_token;
};
After authenticating with Spotify and making the request to /api/token
, you should receive an access token in the response.
We need a place where we can store this key because it will be used in every request we make to Spotify. Saving it in a global variable wouldn’t work because we restart our server every time the file changes. A simple, easy to use, key-value data store is Redis.
Redis
We already installed the redis
dependency in the beginning of the guide. To set it up add the following to src/redis/index.js
const redis = require("redis");
const client = redis.createClient();
…and thats it! With the client
we can get and set keys. However, the redis API is entirely asynchronous. So to set a key it would look something like:
client.get("key", () => {
/* callback function */
});
To make it simpler to work with we can wrap the client methods with promises. See the updated code below
const redis = require("redis");
const { promisify } = require("util");
const client = redis.createClient();
const getValue = promisify(client.get).bind(client);
const setValue = promisify(client.set).bind(client);
The above code wraps the redis client’s get
and set
methods with a promise using promisify
. Now we can read and write values to redis easily in a single line.
await setValue("accessToken", accessToken)
const token = await getValue("accessToken)
console.log("token", token);
Wrapping up Authentication
Back to our authentication code we can now save the access token into redis for safe keeping.
const response = await getAccessToken(requestCode);
const accessToken = response.data.access_token;
await setValue("accessToken", accessToken);
Now that we have our token saved in redis we can begin using the Spotify API to make whatever calls we want!
Creating a Playlist
const createPlaylist = async (options) => {
const accessToken = await getValue("accessToken");
return axios({
url: `https://api.spotify.com/v1/users/${SPOTIFY_USER}/playlists`,
method: "post",
headers: {
Authorization: "Bearer " + accessToken,
},
data: {
public: false,
collaborative: true,
...options,
},
});
};
The createPlaylist
function above uses axios to make a POST
request to Spotify to create a playlist. The access token we retrieved from the previous section is used in the Authorization
header. There are a number of parameters we can pass into the request to configure the playlist. In this example the public
and collaborative
flags are defaulted but can always he overridden. Check the Spotify Web API Reference for more details on creating a playlist.
This function above is asynchronous. We can use await
to wait for the response to come back.
try {
const response = await spotify.createPlaylist(req.body);
const playlistId = response.data.id;
const playlistUrl = response.data.external_urls.spotify
await setValue("currentPlaylistId", playlistId;
res.send(playlistUrl);
} catch (error) {
console.log("Error creating playlist: ", error);
res.status(500).send("Error creating playlist");
}
From the response we can get the the playlist URL, Id and a number of other things. In this example we save the current playlist ID to redis so we can easily access this playlist later. Once the playlist is created successfully, we can send back the URL of it so we can navigate to it.
Add Songs to the Playlist
In a similar fashion, we can add songs to the playlist we just created. First we need to create a function that makes the POST
request to Spotify. See the Spotify Web API Reference for more details on adding a song.
const addSongToPlaylist = async (songs) => {
const accessToken = await getValue("accessToken");
const currentPlaylistId = await getValue("currentPlaylistId");
return axios({
url: `https://api.spotify.com/v1/playlists/${currentPlaylistId}/tracks`,
method: "post",
headers: {
Authorization: "Bearer " + accessToken,
"Content-Type": "application/json",
},
data: {
uris: songs,
},
});
};
The body of the request is a data
object containing an array of song URIs. Then we can call this function with the songs we want to add:
app.post("/playlist", () => {
try {
const songUri = "spotify:track:5sIx4BlfYGuZeSLF40N9GH";
const response = await addSongToPlaylist([songUri]);
res.send(`Successfully added song`);
} catch (error) {
console.log("Error adding song: ", error);
res.status(500).send("Error adding song");
}
}));
Refresh Tokens
The access token is only available for 1 hour. After that hour, the token expires and we have to reauthenticate with Spotify by navigating to the auth URL in the browser. This can be a pain, so to get around this we can request refresh tokens. A refresh token can be sent to Spotify to obtain a brand new access token which can be used for another hour.
In the callback function for the /authorized
endpoint we extracted the access token from the data Spotify sent us after authenticating. We can also extract the refresh token:
setValue("refreshToken", response.data.refresh_token);
Now, we can define a function to get a new access token from the refresh token
const getNewAccessTokenFromRefreshToken = async () => {
try {
const refreshToken = await getValue("refreshToken");
const response = await axios({
url: "https://accounts.spotify.com/api/token",
method: "post",
params: {
grant_type: "refresh_token",
refresh_token: refreshToken,
},
headers: {
Accept: "application/json",
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${auth.encodeAuthorizationToBase64()}`,
},
});
const newAccessToken = response.data.access_token;
await setValue("accessToken", newAccessToken);
return newAccessToken;
} catch (error) {
console.log("Error Getting Refreshed Access Token: ", error);
}
};
This is very similar to the request we made earlier to retrieve the original access token. However, grant_type
is now refresh_token
and we pass in the refresh token we saved to the refresh_token
parameter. Once the request succeeds, the new access token is available via the response.data.access_token
field.
After an hour has passed since we authenticated in the browser, we can no longer use the access token to hit the API. Instead of writing logic to automatically make requests to refresh the token every hour, we can execute this function before making any request to Spotify. This way, no matter how much time has passed, we always ask for a brand new access token and use that to make calls to the API. This is possible because the refresh token we receive never expires.
We can update our function to add a song to the current playlist. The call to getNewAccessTokenFromRefreshToken
will set a new value in redis for accessToken
and then we can use that to make an API call.
const addSongToPlaylist = async (songs) => {
// This line is new
await spotify.getNewAccessTokenFromRefreshToken();
const accessToken = await getValue("accessToken");
const currentPlaylistId = await getValue("currentPlaylistId");
return axios({
url: `https://api.spotify.com/v1/playlists/${currentPlaylistId}/tracks`,
method: "post",
headers: {
Authorization: "Bearer " + accessToken,
"Content-Type": "application/json",
},
data: {
uris: songs,
},
});
};
Summary
That’s it! We have seen how we can set up a local server which gets pinged when we authenticate our Spotify account. We made requests to retrieve access tokens and use those tokens in requests to create playlists and add songs using the API. We used refresh tokens to receive new access tokens and prevent reauthenticating every hour.