Create a Spotify Playlist Generator with Arcjet Protection

Introduction

Web applications are essential for businesses to deliver digital services, and they have become increasingly important in recent years as more and more people access services online.

As web applications become more complex and handle increasingly sensitive data, the need to secure these applications from various threats becomes ever more critical.

In this tutorial, we will build a Spotify playlist generator that can generate personalized music recommendations and secure that with Arcjet, a powerful security framework designed to protect web applications from a wide range of threats.

Let’s dive deep!

Project Setup

Create Node.js Project:

First, we’ll create a Simple Node Js Project with the following Command:

npm init -y
Enter fullscreen mode
Exit fullscreen mode

This command will create a package.json file with default settings. The -y flag automatically answers “yes” to all prompts, allowing for a quick setup.

Install Dependencies:

Next, we’ll install the required packages by running the following command:

npm i express ejs spotify-web-api-node @arcjet/node express-session
Enter fullscreen mode
Exit fullscreen mode

This will install the following packages:

  • express: A popular web framework for Node.js

  • ejs: A simple templating language that lets you generate HTML markup with plain JavaScript

  • spotify-web-api-node: A wrapper for the Spotify Web API

  • @arcjet/node: Arcjet SDK for securing Node.js applications

  • dotenv: Loads environment variables from a .env file

  • express-session: Middleware for managing sessions in Express applications.

Setup Environment Variables:

Next, we’ll create a .env folder to securely store our sensitive information such as API credentials.

Spotify Setup:

For Spotify, go to Spotify Developer Dashboard , click on the Create an app button, and enter the following information:

  • App Name: Spotify Playlist Generator

  • App Description: This Application to generate a playlist based on the user’s favourite artist and mood

  • Redirect URI:  http://localhost:3000/callback.

Finally, check the Developer Terms of Service checkbox and tap on the Create button. This will create a new Spotify application.

Once the app is created, we’ll get the client ID and client secret from the Dashboard and add them to our .env file.

//.env
SPOTIFY_CLIENT_ID=Your_Spotify_Client_ID
SPOTIFY_CLIENT_SECRET=Your_Spotify_Client_Secret
SPOTIFY_REDIRECT_URI=http://localhost:3000/callback
Enter fullscreen mode
Exit fullscreen mode

Arcjet Setup:

Similarly, you need to set up your Arcjet account to obtain the API key:

  1. Create a free account on Arcjet.

  2. After logging in, we’ll create a new site. This will generate an API key for your site.

Now, let’s add the Arcjet API key to our .env file:

//.env
ARCJET_KEY=Your_Arcjet_Key
Enter fullscreen mode
Exit fullscreen mode

Adding Session Secret:

Finally, we’ll add our session secret to the .env file:

//.env
SESSION_SECRET=Your_Session_token
Enter fullscreen mode
Exit fullscreen mode

Create Express Server:

Now, we’ll create an index.js file in the root directory and set up a basic express server. See the following code:

import express from 'express';
import dotenv from 'dotenv'
dotenv.config(); const app = express();
const port = process.env.PORT || 3000; //middleware provided by Express to parse incoming JSON requests.
app.use(express.json()); 
app.use(express.urlencoded({ extended: true }));
app.get('/', (req, res) => {
    res.send('Hello World!');
}) app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});
Enter fullscreen mode
Exit fullscreen mode

Here, We’re using the “dotenv” package to access the PORT number from the .env file.

At the top of the project, we’re loading environment variables using dotenv.config() to make them accessible throughout the file.

Run Project:

Next, we’ll add a start script to the package.json file to easily run our project.

By using the command node index.js, we have to restart your server each time when you make changes to your file. To avoid this we can install nodemon using the following command:

npm install nodemon
Enter fullscreen mode
Exit fullscreen mode

Add the following scripts to your Package.json file:

"scripts": { "start": "nodemon index.js"
}
Enter fullscreen mode
Exit fullscreen mode

The package.json file should look like this:

A JSON configuration file named

To check whether everything is working or not, let’s run the project using the following command:

npm run start
Enter fullscreen mode
Exit fullscreen mode

This will start the Express server. Now if we go to this URL http://localhost:3000/ we’ll get this:

A browser window displays the text

With this, our basic project setup is done. Next, we’ll add functionalities to it.

Project Building

Create the Spotify Client

To begin, we need to set up the Spotify client, which will let our application to authenticate with Spotify and make API requests.

import SpotifyWebApi from 'spotify-web-api-node'; const spotifyApi = new SpotifyWebApi({ clientId: process.env.SPOTIFY_CLIENT_ID, clientSecret: process.env.SPOTIFY_CLIENT_SECRET, redirectUri: process.env.SPOTIFY_REDIRECT_URI,
});
Enter fullscreen mode
Exit fullscreen mode

This initializes a Spotify API client with the necessary credentials and configuration.

Session Management Middleware:

To manage user sessions, we will add session management middleware to our application**.**

import session from 'express-session'; app.use(session({ secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true, cookie: { secure: false }
}));
Enter fullscreen mode
Exit fullscreen mode

This middleware will handle session management, allowing us to store and retrieve session data such as access tokens.

Create the Login Route

With the Spotify client set up, we’ll next create a /login route to handle user authentication. This route will redirect users to Spotify’s authorization page.

app.get('/login', (req, res) => { const scopes = ['playlist-modify-public', 'playlist-modify-private']; const authorizeURL = spotifyApi.createAuthorizeURL(scopes); res.redirect(authorizeURL);
});
Enter fullscreen mode
Exit fullscreen mode

Here, we’ve defined the scopes required for our application, such as playlist-modify-public and playlist-modify-private, and we’ll use the createAuthorizeURL method from the Spotify API client to generate the authorization URL.

We’ll also create a /callback route to handle the callback from Spotify after the user authorizes our application.

app.get('/callback', async (req, res) => { const { code } = req.query; try { const data = await spotifyApi.authorizationCodeGrant(code); req.session.accessToken = data.body['access_token']; req.session.refreshToken = data.body['refresh_token']; spotifyApi.setAccessToken(req.session.accessToken); spotifyApi.setRefreshToken(req.session.refreshToken); res.redirect('/'); } catch (err) { console.error('Error during authorization', err); res.status(500).send('Authorization Error'); }
});
Enter fullscreen mode
Exit fullscreen mode

This route captures the authorization code from the query parameters, requests an access token and a refresh token from Spotify, and stores the access token in the session. It also sets the access and refresh tokens in the Spotify API client for subsequent requests.

This step is really important as without this step, the user can’t add the generated playlist to their Spotify album.

Implement Authentication Middleware

We’ll implement an authentication check to secure our routes and ensure that only authenticated users can access certain functionalities. For that, let’s create a checkAuth middleware function

const checkAuth = (req, res, next) => { if (!req.session.accessToken) { return res.redirect('/'); } next();
};
Enter fullscreen mode
Exit fullscreen mode

This function will check if an access token is present. If not, it will redirect the user to the home page. If the access token exists, the middleware will call next() to proceed to the next middleware or route handler.

Additionally, we will create a middleware to refresh the access token if it has expired:

const refreshAccessToken = async (req, res, next) => { if (req.session.accessToken && req.session.refreshToken) { try { const data = await spotifyApi.refreshAccessToken(); req.session.accessToken = data.body['access_token']; spotifyApi.setAccessToken(req.session.accessToken); next(); } catch (error) { console.error('Error refreshing access token', error); res.status(500).send('Internal Server Error'); } } else { next(); }
};
Enter fullscreen mode
Exit fullscreen mode

Create the Generate Playlist Route

Now, we’ll create the /generate-playlist route which will create the playlist :

app.post('/generate-playlist', checkAuth, async (req, res) => {   const { artistName, mood } = req.body;
  try {     const artistData = await spotifyApi.searchArtists(artistName);
    if (artistData.body.artists.items.length === 0) {
      return res.status(404).send('Artist not found');
    }     const artistId = artistData.body.artists.items[0].id;
    const recommendations = await spotifyApi.getRecommendations({
      seed_artists: [artistId],
      seed_genres: [mood],
      limit: 12,
    });     const tracks = recommendations.body.tracks.map(track => ({
      name: track.name,
      album: track.album.name,
      artists: track.artists.map(artist => artist.name).join(', '),
      duration: ${Math.floor(track.duration_ms / 60000)}:${((track.duration_ms % 60000) / 1000).toFixed(0).padStart(2, '0')},
      uri: track.uri,
      external_url: track.external_urls.spotify,
    }));     res.render('playlist', { tracks });
  } catch (error) {
    console.error('Error generating playlist:', error);
    res.status(500).send('Internal Server Error');
  }
});
Enter fullscreen mode
Exit fullscreen mode

Here, we’re taking the artist name and Mood from the user which are used to search for the artist and generate track recommendations. The searchArtists method searches for the artist and the getRecommendations method generates track recommendations based on the artist and mood.

Additionally, we have structured the generated tracks into a format that can be easily rendered as cards on the frontend.

Saving the Playlist:

Next up we’ll create a /save-playlist endpoint to save the generated playlist to the user’s Spotify account:

app.post('/save-playlist', checkAuth, async (req, res) => {   const { playlistName, trackUris } = req.body;   try {
    const userData = await spotifyApi.getMe();
    const userId = userData.body.id;
    const newPlaylist = await spotifyApi.createPlaylist(userId, {
      name: playlistName,
      public: false
    });     await spotifyApi.addTracksToPlaylist(newPlaylist.body.id, JSON.parse(trackUris));
    res.status(200).send(`Playlist '${playlistName}' created successfully!`);
  } catch (error) {
    console.error('Error creating playlist:', error);
    if (error.response) {
      console.error('Spotify API response:', error.response);
      res.status(error.response.status).send(error.response.data);
    } else {
      res.status(500).send('Internal Server Error');
    }
  }
});
Enter fullscreen mode
Exit fullscreen mode

In this route, we retrieve the user’s Spotify ID using the getMe method. We then create a new playlist with the specified name using the createPlaylist method. Finally, we add the tracks to the playlist using the addTracksToPlaylist method. The track URIs are parsed from the request body and added to the playlist.

Adding the User Interface

Now we’ll integrate EJS templating engine to create a user-friendly interface. EJS allows us to embed JavaScript code within our HTML templates.

For that, we have to set EJS as the view engine in app.js:

app.set('view engine', 'ejs'); app.get('/', (req, res) => {
  res.render('index', { loggedIn: !!req.session.accessToken });
});
Enter fullscreen mode
Exit fullscreen mode

In the code above, we set EJS as the view engine using app.set('view engine', 'ejs') . When a user visits the home page (‘/’), we render the index view and pass a variable loggedIn to determine if the user is logged in.

Creating the Home Page View

Next, let’s create the index.ejs file in the views directory. This file will serve as the home page of our application.

<!DOCTYPE html>
<html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Spotify Playlist Generator</title> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet" /> <style> body { font-family: "Roboto", sans-serif; margin: 0; padding: 0; background-color: #121212; color: #fff; display: flex; justify-content: center; align-items: center; height: 100vh; } .container { max-width: 600px; width: 100%; background: linear-gradient(to bottom, #1db954, #0f813f); background-size: cover; padding: 30px; border-radius: 12px; box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); text-align: center; transition: background-color 0.3s ease; margin: 10px; } h1 { margin-bottom: 20px; font-size: 3em; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-top: 30px; margin-bottom: 8px; font-weight: 500; } .form-group input, .form-group select { width: 90%; padding: 12px; border: none; border-radius: 6px; margin: 10px; font-size: 1em; background-color: #f2f2f2; color: #333; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); transition: box-shadow 0.3s ease; } .form-group select { width: 95%; } .form-group input:focus, .form-group select:focus { outline: none; /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); */ box-shadow: 0 0 0 4px rgba(5, 56, 111, 0.5); } .form-group button { padding: 10px 16px; margin-top: 20px; background: linear-gradient(to right, #07413e, #000f0a); color: #fff; border-radius: 6px; font-size: 1.2em; cursor: pointer; transition: transform 0.3s; } .form-group button:hover { transform: scale(1.1); background: linear-gradient(to right, #156327, #034515); } .login-button { border: 4px solid white; } </style> </head> <body> <div class="container"> <h1>Spotify Playlist Generator</h1> <% if (!loggedIn) { %> <div class="form-group"> <button class="login-button" onclick="window.location.href='/login'"> Login with Spotify </button> </div> <% } else { %> <form action="/generate-playlist" method="POST"> <div class="form-group"> <label for="artist">Artist Name:</label> <input type="text" id="artist" name="artistName" required /> </div> <div class="form-group"> <label for="mood">Mood:</label> <select id="mood" name="mood" required> <option value="happy">Happy</option> <option value="romantic">Romantic</option> <option value="sad">Sad</option> <option value="energetic">Energetic</option> <option value="calm">Calm</option> </select> </div> <div class="form-group"> <button type="submit">Generate Playlist</button> </div> </form> <% } %> </div> </body>
</html>
Enter fullscreen mode
Exit fullscreen mode

In this view, we use EJS syntax (<% %>) to conditionally render content based on the loggedIn variable. If the user is not logged in, a login button is displayed. If the user is logged in, a form is displayed where they can input the artist name and mood to generate a playlist.

Creating the Playlist View

Next, let’s create the playlist.ejs file in the views directory. This file will display the generated playlist.

<!DOCTYPE html>
<html lang="en">
<head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Generated Playlist</title> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> <style> body { font-family: 'Roboto', sans-serif; margin: 0; padding: 0; background-color: #121212; color: #fff; display: flex; justify-content: center; align-items: center; min-height: 100vh; } .container { max-width: 1200px; width: 100%; background: #1db954; padding: 30px; border-radius: 12px; box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); text-align: center; } h1 { margin-bottom: 20px; font-size: 2.5em; } .tracks { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 15px; } .track { padding: 15px; background: #191414; border-radius: 8px; text-align: left; transition: transform 0.3s ease, box-shadow 0.3s ease; display: flex; flex-direction: column; justify-content: space-between; height: 200px; /* Ensure uniform height */ } /* .track:first-of-type { margin-top: 20px; } */ .track:hover { transform: translateY(-5px); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3); } .track strong { font-size: 1.2em; } .track a { color: #1db954; text-decoration: none; } .track a:hover { text-decoration: underline; } /* .form-group { margin-top: 20px; } */ .form-group label { display: block; margin-bottom: 8px; font-weight: 500; } .form-group input { width: 100%; padding: 12px; border: none; border-radius: 6px; margin-bottom: 10px; font-size: 1em; } .form-group button { padding: 10px 16px; margin-top: 20px; background: linear-gradient(to right, #07413e, #000f0a); color: #fff; border-radius: 6px; font-size: 1.2em; cursor: pointer; transition: transform 0.3s; } .form-group button:hover { transform: scale(1.1); background: linear-gradient(to right, #156327, #034515); } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-top: 30px; margin-bottom: 8px; font-weight: 500; } .form-group input, .form-group select { width: 90%; padding: 12px; border: none; border-radius: 6px; margin: 10px; font-size: 1em; background-color: #f2f2f2; color: #333; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1); transition: box-shadow 0.3s ease; } .form-group select { width: 95%; } .form-group input:focus, .form-group select:focus { outline: none; /* box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); */ box-shadow: 0 0 0 4px rgba(5, 56, 111, 0.5); } .modal { display: none; position: fixed; z-index: 1; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.5); justify-content: center; align-items: center; } .modal-content { background-color: #1db954; padding: 20px; border-radius: 8px; width: 80%; max-width: 500px; text-align: center; } .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; } .close:hover, .close:focus { color: #000; text-decoration: none; cursor: pointer; } </style>
</head>
<body> <div class="container"> <h1>Generated Playlist</h1> <div class="tracks"> <% tracks.forEach(track => { %> <div class="track"> <div> <strong><%= track.name %></strong> by <%= track.artists %><br><br> <div>Album: <%= track.album %></div><br><br> <div class="">Duration: <%= track.duration %></div><br><br> </div> <a href="<%= track.external_url %>" target="_blank">Listen on Spotify</a> </div> <% }) %> </div> <button onclick="document.getElementById('playlistModal').style.display='block'">Create New Playlist</button> </div> <div id="playlistModal" class="modal"> <div class="modal-content"> <span class="close" onclick="document.getElementById('playlistModal').style.display='none'">&times;</span> <h2>Create New Playlist</h2> <form action="/save-playlist" method="POST"> <input type="hidden" name="trackUris" value="<%= JSON.stringify(tracks.map(track => track.uri)) %>"> <div class="form-group"> <label for="playlistName">Playlist Name:</label> <input type="text" id="playlistName" name="playlistName" required> </div> <div class="form-group"> <button type="submit">Save Playlist to Spotify</button> </div> </form> </div> </div> <script> // Close the modal when clicking outside of it window.onclick = function(event) { const modal = document.getElementById('playlistModal'); if (event.target == modal) { modal.style.display = 'none'; } } </script>
</body>
</html>
Enter fullscreen mode
Exit fullscreen mode

In this view, we display the generated playlist in a grid layout. Each track is displayed with its name, artists, album, duration, and a link to listen on Spotify.

We also include a button to create a new playlist, which opens a modal where the user can enter a playlist name and save it to their Spotify account.

Securing our Project using Arcjet

So far, we’ve built a Spotify Playlist Generator project. But what if it gets hit by spam API requests, SQL injections, or cross-site scripting attacks?

We don’t want our server to crash or our application to be compromised! To prevent these issues, we’ll add a security layer using Arcjet to protect our application.

What is Arcjet?

Arcjet is a security platform designed to protect web applications from various types of cyber threats, such as spam API requests, SQL injections, and cross-site scripting (XSS) attacks.

It provides a robust security layer that can be easily integrated into web applications to ensure they remain secure and operational even under attack.

Features

  • Signup form protection: Arcjet’s server-side email verification is configured to block disposable providers and ensure that the domain has a valid MX record.

  • Bot protection: Protects the route from automated

  • Rate limiting: Allows different rate limit configurations based on the user’s authentication status. For example, logged-in users can make more requests than anonymous users..

  • Attack protection: Detects and blocks suspicious behavior, such as SQL injection and cross-site scripting (XSS) attacks.

Implementing Arcjet shield to the project

To implement Arcjet shield in our project, let’s create a new Arcjet object with our API key and rules. This should be outside of the request handler.

import arcjet, { detectBot, shield, fixedWindow } from '@arcjet/node'; const aj = arcjet({ key: process.env.ARCJET_KEY, rules: [ shield({ mode: "LIVE", }), fixedWindow({ mode: "LIVE", characteristics: ["ip.src"], match:"/generate-playlist", window: "1m", max: 1, }), detectBot({ mode: "LIVE", block: [ "AUTOMATED", ], patterns: { remove: [ "^curl", ], }, }), ],
});
Enter fullscreen mode
Exit fullscreen mode

Here, we’ve added multiple layers of security to our application:

  • General Protection: The shield rule provides a broad layer of protection against common attacks, including the OWASP Top 10.

  • Rate Limiting: The fixedWindow rule helps prevent abuse by limiting the number of requests to the /generate-playlist endpoint.

  • Bot Detection: The detectBot rule helps identify and block automated bot traffic, ensuring that only legitimate users can access your application.

Now, We’ll create a Middleware function to check if the request is secure. If not, the middleware will throw an error and end the request. Otherwise, it will allow the request to proceed.

app.use(async (req, res, next) => {
  try {
    const decision = await aj.protect(req);
    if (decision.isDenied()) {       console.error("Arcjet protection denied", decision);
      res.writeHead(403, { "Content-Type": "application/json" });
      res.end(JSON.stringify({ error: "Forbidden" }));     } else {
      next();
    }
  } catch (error) {
    console.error("Arcjet protection error", error);
    res.status(500).send({ error: 'Internal Server Error' });
  }
});
Enter fullscreen mode
Exit fullscreen mode

Note: To Test Arcjet in the Development server. We need to add the following in our env folder:

//.env ARCJET_ENV=development

This will allow private/internal addresses so that the SDKs work correctly locally.

You can get the whole code here: https://github.com/Arindam200/spotify-playlist-generator

Running it locally:

To run the project, let’s run the following command in our terminal:

npm run dev
Enter fullscreen mode
Exit fullscreen mode

This will Start Our Server:

A command line interface showing the output of a script. The path displayed is “, and the command executed is “npm run dev”. The script uses nodemon to monitor changes and outputs “Server running on port 3000”.”/>

Now, Let’s Go to localhost:3000 Here, we will get the initial user interface. At this point, we need to log in to our Spotify account to proceed.

After successfully logging in, we’ll get a Form where we can enter our favorite artist and select a mood from the dropdown menu.

A green interface for a Spotify Playlist Generator with fields for

After adding them, let’s click on the “Generate Playlist” button. This action will trigger the backend logic to create a playlist based on our inputs.

A generated playlist page displaying ten songs with details including titles, artists, album names, and durations. Each song has an option to listen on Spotify. The songs listed are

If we want to reload this page it will throw an error due to rate limits that we have configured in the previous section. The application is configured to allow only one playlist creation request per minute. This means we can only make one request within a one-minute window.

A browser window with a JSON error message: {

We can also see the error in our terminal:

Screenshot of a server log showing the status of Arcjet protection decisions. The log indicates the server is running on port 3000 and includes multiple ArcjetRuleResult entries with details such as , , , , and . Various conclusions include 'ALLOW' and 'DENY', and reasons like ArcjetShieldReason, ArcjetRateLimitReason, and ArcjetBotReason. IP details with undefined latitude are also mentioned.

Now, let’s look at our Arcjet dashboard. Here, we will see all the requests made to our application. This dashboard provides a comprehensive overview of our application’s activity.

A screenshot of a web application interface showing a list of HTTP requests under the

We can also inspect each request in detail. If a request is denied, the dashboard will provide information on the reason for the denial, helping us to understand and address any issues:

A screenshot of a web application interface showing request logs. One log entry is highlighted in red, indicating a request to

And that’s it! We have successfully set up and Secured our Spotify Playlist Generator project.

Note: Arcjet reached out to me, inviting me to participate in their beta testing program and share my experience. While they did compensate me for my time, they did not influence the content of this write-up.

Conclusion

In this tutorial, we explored how to build a Spotify Playlist Generator and secure it using Arcjet. By integrating Arcjet’s robust protection mechanisms, we ensured that our application is safeguarded against unauthorized access and potential threats.

Now that you’ve learned how to integrate Arcjet for securing your application, you can leverage its powerful features to protect your applications in real-world scenarios.

If you found this helpful, feel free to share this with your friends. Also, For any queries connect with me on Twitter, LinkedIn, Youtube and GitHub.

Thanks for Reading.


Discover more from Coursity

Subscribe to get the latest posts sent to your email.

Leave a Comment

Your email address will not be published. Required fields are marked *

Discover more from Coursity

Subscribe now to keep reading and get access to the full archive.

Continue reading

Scroll to Top