Music Discovery App with MERN Stack
In today’s digital age, music discovery has become an integral part of our lives. With the rise of streaming platforms and personalized recommendations, users are constantly seeking new music experiences. In this article, we’ll delve into the creation of a Music Discovery App using the MERN stack (MongoDB, Express.js, React.js, Node.js). This comprehensive guide will walk you through the process of building a feature-rich web application for exploring and discovering music.
Output Preview:
Prerequisites
Approach
- Users can view a list of songs, search by title, artist, or genre, and filter results based on their preferences.
- Users can add new songs to the database by providing details such as title, artist, genre, and release date.
- Existing songs can be edited to update information or deleted if no longer needed.
- The app’s user interface is designed to be responsive and user-friendly, accessible on both desktop and mobile devices.
Steps to Setup The Backend
Step 1: Create a new directory for the backend and navigate to the backend directory
mkdir music-backend
cd music-backend
Step 2: Initialize a new Node.js project
npm init -y
Step 3: Install the necessary packages/libraries in your project using the following commands.
npm install express mongoose dotenv cors
Project Structure:
The updated dependencies in package.json file will look like:
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"express": "^4.18.3",
"mongoose": "^8.2.2"
}
Example: Implementation to setup the frontend for the music discovery app.
// server.js
const express = require('express');
const mongoose = require('mongoose');
const cors = require('cors');
require('dotenv').config();
const app = express();
const PORT = process.env.PORT || 3001;
app.use(express.json());
app.use(cors());
// MongoDB connection
mongoose.connect("mongodb://localhost:27017/musicdiscovery",
{
useNewUrlParser: true,
useUnifiedTopology: true
});
const db = mongoose.connection;
db.on('error',
console.error.bind(
console, 'MongoDB connection error:'));
db.once('open',
() => console.log('Connected to MongoDB'));
// Music model
const musicSchema = new mongoose.Schema({
title: String,
artist: String,
genre: String,
releaseDate: Date,
songUrl: String, // Added songUrl field
});
const Music = mongoose.model('Music', musicSchema);
// Routes
app.get('/api/music', async (req, res) => {
try {
const { search } = req.query;
let query = {};
if (search) {
query = {
$or: [
// Case-insensitive search for title
{ title: { $regex: search, $options: 'i' } },
// Case-insensitive search for artist
{ artist: { $regex: search, $options: 'i' } },
// Case-insensitive search for genre
{ genre: { $regex: search, $options: 'i' } },
],
};
}
const music = await Music.find(query);
res.json(music);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.post('/api/music', async (req, res) => {
try {
const {
title, artist, genre,
releaseDate, songUrl } = req.body;
const newMusic =
new Music({
title, artist, genre,
releaseDate, songUrl
});
await newMusic.save();
res.status(201).json(newMusic);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Edit music endpoint
app.put('/api/music/:id', async (req, res) => {
try {
const { id } = req.params;
const {
title, artist, genre,
releaseDate, songUrl
} = req.body;
const updatedMusic =
await Music.findByIdAndUpdate(
id, {
title, artist, genre,
releaseDate, songUrl
}, { new: true });
res.json(updatedMusic);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Delete music endpoint
app.delete('/api/music/:id', async (req, res) => {
try {
const { id } = req.params;
await Music.findByIdAndDelete(id);
res.json({ message: 'Music deleted successfully.' });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Start server
app.listen(PORT,
() => console.log(`Server running on port ${PORT}`));
Step to Run Application: Run the application using the following command from the root directory of the project
node server.js
Steps to Setup The Frontend
Step 1: Run the command to create a new React app using create-react-app:
npx create-react-app music-discovery-app
Step 2: Navigate to App Directory:
cd music-discovery-app
Step 3: Install the necessary packages/libraries in your project using the following commands.
npm install axios
Project Structure:
The updated dependencies in package.json file 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.8",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"web-vitals": "^2.1.4"
}
Example: Implementation to setup the frontend for the music discovery app.
/* App.css */
body {
font-family: Arial, sans-serif;
background: linear-gradient(
179.4deg, rgb(12, 20, 69) -16.9%, rgb(71, 30, 84) 119.9%);
color: #ffffff;
margin: 2%
}
h1 {
text-align: center;
margin-bottom: 20px;
}
form {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-bottom: 20px;
}
input {
padding: 8px;
margin: 4px;
border: 1px solid #ffffff;
border-radius: 4px;
background-color: rgba(244, 239, 239, 0.1);
color: #000000;
}
button {
padding: 8px 16px;
margin: 4px;
border: none;
border-radius: 4px;
background-color: #ffffff;
color: #2c5d63;
cursor: pointer;
}
.music-item {
border: 1px solid #ccc;
border-radius: 5px;
margin-bottom: 10px;
padding: 10px;
}
.playing {
background: linear-gradient(
to right, rgb(173, 83, 137), rgb(60, 16, 83)) !important;
/* Light blue background color for playing song */
}
ul {
list-style-type: none;
padding: 0;
}
li {
margin-bottom: 8px;
}
li strong {
font-weight: bold;
}
.music-list {
display: flex;
justify-content: center;
flex-direction: row;
flex-wrap: wrap;
gap: 20px;
margin-top: 20px;
}
.music-item {
width: 600px;
border: 1px solid #ffffff;
border-radius: 8px;
padding: 20px;
background-color: rgba(255, 255, 255, 0.1);
}
input {
color: white;
}
.music-info h3 {
margin-bottom: 10px;
}
.music-info p {
margin: 5px 0;
}
.search-container {
margin-top: 20px;
display: flex;
justify-content: center;
align-items: center;
}
.search-container input[type="text"] {
padding: 8px;
margin-right: 10px;
border: 1px solid #ffffff;
border-radius: 4px;
background-color: rgba(255, 255, 255, 0.1);
color: #ffffff;
}
.search-container button {
padding: 8px 16px;
border: none;
border-radius: 4px;
background-color: #ffffff;
color: #2c5d63;
cursor: pointer;
}
//App.js
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';
function App() {
const [musicList, setMusicList] = useState([]);
const [newMusic, setNewMusic] =
useState({
title: '', artist: '',
genre: '', releaseDate: '',
songUrl: ''
});
const [searchTerm, setSearchTerm] = useState('');
const [currentSong, setCurrentSong] = useState(null);
const [audioPlayer, setAudioPlayer] = useState(null);
// Track playing state
const [isPlaying, setIsPlaying] = useState(false);
useEffect(() => {
axios.get('http://localhost:3001/api/music')
.then(response => setMusicList(response.data))
.catch(error =>
console.error('Error fetching music:', error));
}, []);
const handleSearch = () => {
// Trim whitespace from search term
let searchQuery = searchTerm.trim();
if (searchQuery === '') {
// If search query is empty, fetch all songs
axios.get('http://localhost:3001/api/music')
.then(response =>
setMusicList(response.data))
.catch(error =>
console.error('Error fetching music:', error));
} else {
// Otherwise, perform search with non-empty query
axios.get(
`http://localhost:3001/api/music?search=${searchQuery}`)
.then(response => setMusicList(response.data))
.catch(error =>
console.error('Error searching music:', error));
}
};
const handleInputChange = e => {
const { name, value } = e.target;
setNewMusic(
prevState => ({ ...prevState, [name]: value }));
};
const handleSubmit = e => {
e.preventDefault();
if (!newMusic.title ||
!newMusic.artist ||
!newMusic.genre ||
!newMusic.releaseDate ||
!newMusic.songUrl) {
console.error('Please fill in all fields.');
return;
}
axios.post('http://localhost:3001/api/music', newMusic)
.then(response => {
setMusicList(
prevState => [...prevState, response.data]);
setNewMusic({
title: '', artist: '',
genre: '', releaseDate: '',
songUrl: ''
});
console.log('Music added successfully.');
})
.catch(error =>
console.error('Error adding music:', error));
};
const handleEdit = (id, currentMusic) => {
let updatedTitle =
prompt("Enter updated title:", currentMusic.title);
let updatedArtist =
prompt("Enter updated artist:", currentMusic.artist);
let updatedGenre =
prompt("Enter updated genre:", currentMusic.genre);
let updatedReleaseDate =
prompt("Enter updated release date (YYYY-MM-DD):",
currentMusic.releaseDate);
let updatedSongUrl =
prompt("Enter updated song URL:",
currentMusic.songUrl);
// Check if any field is null or undefined
if (updatedTitle === null || updatedTitle === undefined ||
updatedArtist === null || updatedArtist === undefined ||
updatedGenre === null || updatedGenre === undefined ||
updatedReleaseDate === null ||
updatedReleaseDate === undefined ||
updatedSongUrl === null || updatedSongUrl === undefined) {
console.error('Please fill in all fields.');
return;
}
// Construct updatedMusic object
const updatedMusic = {
title: updatedTitle,
artist: updatedArtist,
genre: updatedGenre,
releaseDate: updatedReleaseDate,
songUrl: updatedSongUrl
};
axios.put(
`http://localhost:3001/api/music/${id}`, updatedMusic)
.then(response => {
const updatedList =
musicList.map(music =>
(music._id === id ? response.data : music));
setMusicList(updatedList);
console.log('Music edited successfully.');
})
.catch(error =>
console.error('Error editing music:', error));
};
const handleDelete = id => {
axios.delete(`http://localhost:3001/api/music/${id}`)
.then(() => {
const updatedList =
musicList.filter(
music => music._id !== id);
setMusicList(updatedList);
console.log('Music deleted successfully.');
})
.catch(error =>
console.error('Error deleting music:', error));
};
const playSong = (songUrl) => {
if (audioPlayer) {
// Check if audio is paused before attempting to play
if (audioPlayer.paused) {
audioPlayer.src = songUrl;
audioPlayer.load();
audioPlayer.play();
setIsPlaying(true); // Update playing state
setCurrentSong(songUrl); // Set current song
}
}
};
useEffect(() => {
if (audioPlayer) {
audioPlayer.onended = () => {
setCurrentSong(null);
setIsPlaying(false); // Update playing state
};
}
}, [audioPlayer]);
return (
<div className="App">
<h1>Music Discovery App</h1>
<form onSubmit={handleSubmit}>
<input type="text" name="title"
placeholder="Title" value={newMusic.title}
onChange={handleInputChange} />
<input type="text" name="artist"
placeholder="Artist" value={newMusic.artist}
onChange={handleInputChange} />
<input type="text" name="genre"
placeholder="Genre" value={newMusic.genre}
onChange={handleInputChange} />
<input type="date" name="releaseDate"
value={newMusic.releaseDate}
onChange={handleInputChange} />
<input type="text" name="songUrl"
placeholder="Song URL" value={newMusic.songUrl}
onChange={handleInputChange} />
<button type="submit">Add Music</button>
</form>
<div className="search-container">
<input type="text" placeholder="Search music..."
value={searchTerm} onChange={
(e) => setSearchTerm(e.target.value)} />
<button onClick={handleSearch}>Search</button>
</div>
<div className="music-list">
{musicList.map(music => (
<div className={
`music-item ${currentSong === music.songUrl ? 'playing' : ''}`}
key={music._id}>
<div className="music-info">
<h3>{music.title}</h3>
<p>Artist: {music.artist}</p>
<p>Genre: {music.genre}</p>
<p>
Release Date: {new Date(music.releaseDate).toLocaleDateString()}
</p>
{currentSong === music.songUrl ? (
<button onClick={() => {
audioPlayer.pause();
setIsPlaying(false);
setCurrentSong(null);
}}>Pause</button>
) : (
<button onClick={() => {
playSong(music.songUrl);
setCurrentSong(music.songUrl);
}}>
{isPlaying && currentSong === music.songUrl ? 'Pause' : 'Play'}
</button>
)}
</div>
<div className="music-actions">
<button onClick={
() => handleEdit(music._id,
{ title: 'Updated Title' })}>
Edit
</button>
<button onClick={
() => handleDelete(music._id)}>
Delete
</button>
</div>
</div>
))}
</div>
<audio ref={(ref) => setAudioPlayer(ref)} />
</div>
);
}
export default App;
Step to Run Application: Run the application using the following command from the root directory of the project
npm start
Output: Your project will be shown in the URL http://localhost:3000/
Contact Us