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
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
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
Arcjet Setup:
Similarly, you need to set up your Arcjet account to obtain the API key:
Create a free account on Arcjet.
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
Adding Session Secret:
Finally, we’ll add our session secret to the .env file:
//.env
SESSION_SECRET=Your_Session_token
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}`);
});
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
Add the following scripts to your Package.json file:
"scripts": {
"start": "nodemon index.js"
}
The package.json
file should look like this:
To check whether everything is working or not, let's run the project using the following command:
npm run start
This will start the Express server. Now if we go to this URL http://localhost:3000/ we'll get this:
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,
});
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 }
}));
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);
});
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');
}
});
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();
};
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();
}
};
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');
}
});
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');
}
}
});
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 });
});
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>
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'">×</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>
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",
],
},
}),
],
});
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' });
}
});
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
This will Start Our Server:
", 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.
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.
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.
We can also see the error in our terminal:
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.
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:
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.
Top comments (3)
Nice Write up!
Thanks Hemath!
Cool!