Music Playlist App using MERN Stack
This tutorial will create a Music Playlist App using the MERN (MongoDB, Express.js, React.js, Node.js) stack. The app allows users to manage playlists, add songs, and play their favorite tracks. We’ll cover front and backend development, integrating features like song uploading, playlist creation, and playback functionality.
Output Preview: Let us have a look at how the final output will look like.
Prerequisites
- Node.js and npm
- MongoDB
- Express.js
- React.js
- Axios for API requests
- Multer for file uploading
- GridFS for storing audio files in MongoDB
Approach to create Music Playlist App:
- Create a Node.js project with Express.js.
- Connect to MongoDB using mongoose.
- Implement RESTful API endpoints for managing playlists and songs.
- Initialize a React.js project.
- Design components to display playlists and songs.
- Use Axios to fetch data from the backend and handle user interactions.
- Implement file upload functionality using Multer and GridFS.
- Apply CSS to style the app and make it visually appealing.
Steps to Create the BackEnd:
Step 1: Set Up Backend Server using following commands:-
mkdir music-playlist-app-backend
cd music-playlist-app-backend
npm init -y
Step 2: Install all the necessary dependencies:-
npm install express mongoose cors multer multer-gridfs-storage
Project Structure(Backend):
The updated dependencies in package.json file of backend will look like:
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"express": "^4.18.3",
"gridfs-stream": "^1.1.1",
"mongoose": "^8.2.1",
"multer": "^1.4.5-lts.1",
"multer-gridfs-storage": "^5.0.2",
"shortid": "^2.2.16",
"uuid": "^9.0.1"
}
Example: Create `server.js` and write the below code.
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
const bodyParser = require('body-parser');
const multer = require('multer');
const { GridFsStorage } = require('multer-gridfs-storage');
const { GridFSBucket, ObjectId } = require('mongodb');
const shortid = require('shortid');
require('dotenv').config();
const app = express();
// Middleware
app.use(cors());
app.use(bodyParser.json());
// MongoDB Connection
mongoose.connect('mongodb://localhost:27017/musicdatabase', {
useNewUrlParser: true,
useUnifiedTopology: true
})
.then(() => console.log('Connected to MongoDB'))
.catch(err => console.error('Failed to connect to MongoDB', err));
// Initialize GridFS
let gfs;
const conn = mongoose.connection;
conn.once('open', () => {
gfs = new GridFSBucket(conn.db, {
bucketName: 'uploads' // Specify your bucket name here
});
});
// Create storage engine using GridFS
const storage = new GridFsStorage({
url: 'mongodb://localhost:27017/musicdatabase',
file: (req, file) => {
return {
filename: file.originalname,
bucketName: 'uploads' // Bucket name in MongoDB
};
}
});
// Set up multer to handle file uploads
const upload = multer({ storage });
// Route to handle file uploads
app.post('/api/upload', upload.single('audioFile'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'No file uploaded' });
}
console.log('Uploaded file:', req.file);
res.json({ fileId: req.file.id });
});
// Playlist Model
const PlaylistSchema = new mongoose.Schema({
name: { type: String, required: true },
songs: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Song' }],
playlistCode: {
type: String,
required: true,
unique: true,
default: () => shortid.generate()
}
});
const Playlist = mongoose.model('Playlist', PlaylistSchema);
// Song Model
const Song = mongoose.model('Song', new mongoose.Schema({
title: { type: String, required: true },
artist: { type: String, required: true },
songcode: { type: String, required: true, unique: true },
album: String,
duration: { type: Number, required: true },
fileId: { type: mongoose.Schema
.Types.ObjectId, ref: 'uploads.files' }
// Reference to GridFS file
}));
// Routes
// Playlists
app.get('/api/playlists', async (req, res) => {
try {
const playlists = await Playlist.find().populate('songs');
res.json(playlists);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
app.get('/api/songs/:songId/audio', async (req, res) => {
try {
const songId = req.params.songId;
// Ensure the songId is a valid ObjectId
if (!ObjectId.isValid(songId)) {
return res.status(404).json({ error: 'Invalid song ID' });
}
// Find the song in MongoDB
const song = await Song.findById(songId);
if (!song) {
return res.status(404).json({ error: 'Song not found' });
}
// Set the appropriate Content-Type header
res.set('Content-Type', 'audio/wav');
// Modify the Content-Type as per your file format
// Stream the audio file from GridFS
const downloadStream = gfs.openDownloadStream(song.fileId);
// Assuming fileId is the ID of the audio file in GridFS
downloadStream.pipe(res);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
app.get('/api/playlists/:playlistName/songs', async (req, res) => {
try {
const playlistName = req.params.playlistName;
const playlist =
await Playlist.findOne({ name: playlistName })
.populate('songs');
if (!playlist) {
return res.status(404)
.json({ error: 'Playlist not found' });
}
res.json(playlist.songs);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Update the /api/playlists POST endpoint
app.post('/api/playlists', async (req, res) => {
try {
const { name, songs } = req.body;
// Find the ObjectId values of the
// songs specified by their titles
const existingSongs = await Song.find({ title:{$in: songs} });
const songIds = existingSongs.map(song => song._id);
// Validate if all songs were found
if (existingSongs.length !== songs.length) {
const missingSongs =
songs.filter(
song => !existingSongs.find(
existingSong => existingSong.title === song)
);
return res.status(400)
.json(
{
error: `One or more songs not found:
${missingSongs.join(', ')}`
});
}
// Create the playlist with the provided data
const playlist = new Playlist({ name, songs: songIds });
// Save the playlist to the database
await playlist.save();
// Return the created playlist
res.json(playlist);
} catch (err) {
if (err.code === 11000 && err.keyPattern
&& err.keyPattern.songcode) {
return res.status(400)
.json({ error: 'Duplicate songcode found' });
} else {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
}
});
// Collaborative Playlists
app.post('/api/playlists/:playlistId/collaborators',async(req,res) => {
try {
const { userId } = req.body;
const playlist = await Playlist.findByIdAndUpdate(
req.params.playlistId,
{ $addToSet: { collaborators: userId } },
{ new: true }
);
res.json(playlist);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
app.get('/api/playlists/collaborative/:userId', async (req, res) => {
try {
const playlists =
await Playlist.find({ collaborators: req.params.userId });
res.json(playlists);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Songs
app.get('/api/songs', async (req, res) => {
try {
const { title, artist, album } = req.query;
const filter = {};
if (title) filter.title = new RegExp(title, 'i');
if (artist) filter.artist = new RegExp(artist, 'i');
if (album) filter.album = new RegExp(album, 'i');
const songs = await Song.find(filter);
res.json(songs);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
app.post('/api/songs', async (req, res) => {
try {
const { title, artist, album, duration } = req.body;
const song =
new Song({ title, artist, album, duration });
await song.save();
res.json(song);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
// Error handling middleware
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something went wrong!');
});
// Start server
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`Server running on port ${PORT}`));
Start the backend server with the following command:
node server.js
Steps to Create the Frontend:
Step 1: Create a directory named “Songs” for keeping audio.
Step 2: Create FrontEnd using React:-
npx create-react-app music-playlist-frontend
cd music-playlist-frontend
Step 3: Install the required dependencies.
npm install axios
Project Structure(Frontend):
The updated dependencies in package.json file of Frontend will look like:
"dependencies": {
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.6.7",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
Example: Create the required files and write the following code.
/* PlaylistList.css */
.playlist-container {
background-color: #f2f2f2;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.playlist-title {
color: #f96d00;
font-size: 24px;
margin-bottom: 10px;
}
.playlist-item {
color: #333;
font-size: 16px;
margin-bottom: 8px;
}
/* SongList.css */
.song-container {
background-color: #f2f2f2;
padding: 20px;
border-radius: 8px;
}
.song-title {
color: #f96d00;
font-size: 24px;
margin-bottom: 10px;
}
.song-item {
color: #333;
font-size: 16px;
margin-bottom: 8px;
}
/* App.css */
body {
font-family: Arial, sans-serif;
background-color: #f96d00; /* Update background color */
margin: 0;
padding: 0;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
h1 {
color: #f2f2f2; /* Update text color */
}
ul {
list-style: none;
padding: 0;
}
li {
margin-bottom: 10px;
}
input[type="text"] {
width: 200px;
padding: 8px;
border: none;
border-radius: 4px;
margin-right: 10px;
}
button {
background-color: #f2f2f2; /* Update background color */
color: #f96d00; /* Update text color */
border: none;
padding: 10px 20px;
cursor: pointer;
border-radius: 4px;
}
button:hover {
background-color: #f96d00; /* Update background color */
color: #f2f2f2; /* Update text color */
}
.song-item {
display: flex;
justify-content: space-between;
align-items: center;
}
.now-playing {
margin-left: auto;
}
.play-pause-button {
margin-left: 10px; /* Adjust as needed */
}
// App.js
import React from 'react';
import PlaylistList from './Playlist';
import SongList from './Songs';
import './App.css';
function App() {
return (
<div className="container">
<h1>Music Playlist App</h1>
<PlaylistList />
<SongList />
</div>
);
}
export default App;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css'; // Import CSS file
function PlaylistList() {
const [playlists, setPlaylists] = useState([]);
const [newPlaylistName, setNewPlaylistName] = useState('');
const [newPlaylistSongs, setNewPlaylistSongs] = useState([]);
const [hoveredPlaylist, setHoveredPlaylist] = useState(null);
// State to track the hovered playlist ID
useEffect(() => {
axios.get('http://localhost:5000/api/playlists')
.then(response => {
setPlaylists(response.data);
})
.catch(error => {
console.error('Error fetching playlists:', error);
});
}, []);
const handleCreatePlaylist = () => {
axios.post('http://localhost:5000/api/playlists', {
name: newPlaylistName,
songs: newPlaylistSongs,
// Include songs array in the request body
user: 'user_id',
// Replace 'user_id' with the actual user ID
playlistCode: 'your_playlist_code'
// Replace 'your_playlist_code' with
// the actual playlist code
})
.then(response => {
setPlaylists([...playlists, response.data]);
setNewPlaylistName('');
setNewPlaylistSongs([]);
})
.catch(error => {
console.error('Error creating playlist:', error);
});
};
// Function to handle playlist hover
const handleMouseEnter = (playlistId) => {
setHoveredPlaylist(playlistId);
};
// Function to handle leaving playlist hover
const handleMouseLeave = () => {
setHoveredPlaylist(null);
};
return (
<div className="playlist-container">
<h2 className="playlist-title">Playlists</h2>
<ul>
{playlists.map(playlist => (
<li
key={playlist._id}
className="playlist-item"
onMouseEnter={() => handleMouseEnter(playlist._id)}
// Set the hovered playlist ID
onMouseLeave={handleMouseLeave}
// Clear the hovered playlist ID when leaving
style={
{
fontSize: "large",
fontWeight: "bolder",
color: "#3a4750"
}}>
{playlist.name}
{/* Display the songs when the playlist is hovered */}
{
hoveredPlaylist === playlist._id &&
playlist.songs && Array.isArray(playlist.songs) && (
<div className="song-list-container"
style={
{
backgroundColor: "#e3e3e3",
padding: "2%",
margin: "1%"
}}> {/* Container for the song list */}
<h3 className="song-list-title">
Songs
</h3>
<ul className="song-list">
{
playlist.songs.map(song => (
<li key={song._id}>
{song.title}
</li>
))
}
</ul>
</div>
)
}
</li>
))}
</ul>
<input
type="text"
placeholder="Enter playlist name"
value={newPlaylistName}
onChange={e => setNewPlaylistName(e.target.value)} />
<input
type="text"
placeholder="Enter songs (separated by commas)"
value={newPlaylistSongs.join(',')}
onChange={e => setNewPlaylistSongs(e.target.value.split(','))} />
<button onClick={handleCreatePlaylist}>
Create Playlist
</button>
</div>
);
}
export default PlaylistList;
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';
function Songs() {
const [songs, setSongs] = useState([]);
const [currentSong, setCurrentSong] = useState(null);
// State to keep track of the currently playing song
const [audio, setAudio] = useState(null);
// State to keep track of the audio element
useEffect(() => {
axios.get('http://localhost:5000/api/songs')
.then(response => {
setSongs(response.data);
})
.catch(error => {
console.error('Error fetching songs:', error);
});
}, []);
const playSong = (song) => {
if (currentSong === null || currentSong._id !== song._id) {
if (audio) {
audio.pause(); // Pause the current song if any
}
const newAudio =
new Audio(`
http://localhost:5000/api/songs/${song._id}/audio`);
setCurrentSong(song);
setAudio(newAudio);
newAudio.play(); // Play the new song
} else {
if (audio.paused) {
audio.play(); // If paused, resume playing
} else {
audio.pause(); // If playing, pause
}
}
};
return (
<div className="song-container">
<h2 className="song-title">Songs</h2>
<ul>
{
songs.map(song => (
<li key={song._id} className="song-item"
onClick={() => playSong(song)}>
<span>{song.title} - {song.artist}</span>
{currentSong && currentSong._id === song._id && (
<span className="now-playing">
Now playing: {currentSong.title}
</span>
)}
</li>
))
}
</ul>
{currentSong && (
<div>
<button onClick={() => playSong(currentSong)}>
{audio && !audio.paused ? 'Pause' : 'Play'}
</button>
</div>
)}
</div>
);
}
export default Songs;
Start the Frontend Application with the following command:
npm start
Output:
- Browser Output
- Data saved in Database:
Contact Us