Building itunez: A Music Player App with Node.js, Firebase, Tailwind CSS and React.js

Building itunez: A Music Player App with Node.js, Firebase, Tailwind CSS and React.js

Table of contents

No heading

No headings in the article.

Introduction

In this article, we will explore the development process of a music player app called Itunez. The app allows administrators to upload music, along with artist and album information, which is then displayed on the homepage for users to listen to. It makes it easier for users to search music based on categories, artists, albums, and song names. Only the admin has privileged access to the dashboard, where they can manage the app's content. The Itunez project utilizes Node.js for the backend, MongoDB for the database, and Firebase for authentication and storage. The front end is built using React.js, with Redux for state management.

Backend Development

The backend of Itunez is implemented using Node.js, a popular runtime environment for building server-side applications., Restful API is implemented, The backend code is organized into several folders, including the controller, model, route, config, and validations These folders reside in the "src" directory. The controller folder contains the logic for handling various API endpoints, while the model folder defines the data models for the application. The route folder manages the routing of incoming requests to the appropriate controller functions. Configuration files and validation logic are also included to ensure smooth operation. Additionally, an environment file is used to store sensitive environment variables securely.

Database and Authentication

To store the app's data, Itunez employs MongoDB, a NoSQL database known for its flexibility and scalability. MongoDB seamlessly integrates with Node.js, allowing efficient storage and retrieval of music, artist, and album information. For user authentication, Firebase authentication is utilized. Firebase provides secure and easy-to-use authentication services, ensuring that only authorized users can access the app's admin dashboard and User Homepage. let me walk you through the codes.

The models

User model

const mongoose = require("mongoose")

const userSchema = new mongoose.Schema({
    name:{type:String, required:true},
    email:{type:String, required:true},
    imageUrl:{type:String, required:true},
    userId:{type:String, required:true},
    emailVerified:{type:Boolean, required:true},
    role:{type:String, required:true},
    authTime:{type:String, required:true}
},{
    timestamps:true
})


exports.User = mongoose.model('users', userSchema)

Song Model

const mongoose = require("mongoose")

const userSchema = new mongoose.Schema({
    name:{type:String, required:true},
    imageUrl:{type:String, required:true},
    songUrl:{type:String, required:true},
    album:{type:String},
    artist:{type:String, required:true},
    language:{type:String, required:true},
    category:{type:String, required:true},
},{
    timestamps:true
})


exports.Songs = mongoose.model('songs', userSchema)

config/database.js, shows how the database is connected

const mongoose = require('mongoose');
require("dotenv").config()
const port = process.env.PORT || 5000;
const connectDatabase = async function(app){
    try {
        mongoose.set('strictQuery', false);
        mongoose.connect(process.env.DB_URI,{dbName: process.env.DB_NAME}, ()=>{
            console.log("Database Connected");
            app.listen(port, ()=>{
                console.log(`Server listening on ${port}`);
            });
        });
    } catch (error) {
        console.log(error);
    }

}

module.exports = {connectDatabase};

Config/firebase.js ....helps connect to firebase with the help of the serviceAccountKey.json.

var admin = require("firebase-admin");

var serviceAccount = require("../../serviceAccountKey.json");

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount)
});


module.exports = admin

Now to the Controllers, we start with the user, we check for a valid user and save it to the database

const admin = require('../config/firebase');
const User = require("../models/users.model").User;


exports.login= async function (req,res) {
    if (!req.headers.authorization) {
      return  res.status(500).send({message:"invalid token"})
    }
    const token= req.headers.authorization.split(" ")[1];
    try {
        const decodeValue= await admin.auth().verifyIdToken(token)
        if (!decodeValue) {
            res.status(505).json({message:"Un Authorised"})
        }else{
            const userExist= await User.findOne({'userId':decodeValue.user_id})
            if (userExist) {
                const filter={userId:decodeValue.user_id}
               const option={
                upsert:true,
                new:true
               }
               try {
                const user= await User.findOneAndUpdate(filter,{authTime:decodeValue.auth_time},option);
              res.status(200).send(user)
               } catch (error) {
                res.status(400).send({success:false,message:error}) 
               }

              }else {
                const details={
                    name: decodeValue.name,
                    email: decodeValue.email,
                    imageUrl: decodeValue.picture,
                    userId: decodeValue.user_id,
                    role: 'member',
                    authTime:decodeValue.auth_time,
                    emailVerified:decodeValue.email_verified,
                };
                try {
                const user = new User(details);
                user.save();
                res.status(200).send(user)
                } catch (error) {
                   res.status(400).send({success:false,message:error}) 
                }
              }


        }
    }catch (error) {
        res.status(400).send({success:false,message:error}) 
     }
}

, getaUser by passing the userid as params

exports.getUser = async function (req, res) {
  try {
    const data = await User.findById({_id:req.params.id});
    if (!data) {
      return res.status(404).json({ msg: "Not Found" });
    }
    return res.status(200).json(data);
  } catch (error) {
    console.log(error.message);
    if (error.name == "CastError") {
      return res.status(400).json(error.message);
    }
    return res.status(500).json(error);
  }
};

and delete User.

exports.deleteUser = async function (req, res) {
  try {
    const user = await User.findByIdAndDelete({_id:req.params.id});
    if (!user) {
      return res
        .status(404)
        .json({ msg: `No user with id ${req.params.id}` });
    } else {
      return res
        .status(200)
        .json({ msg: "user Deleted Successfully", data: null });
    }
  } catch (error) {
    console.log(error);
  }
};

update user details

exports.updateRole= async function(req,res){
  const role = req.body;
  User.findByIdAndUpdate(req.params.id, role ,{new:true}, (err, data)=>{
      if (data) {
          return  res.status(200).send({success:true, user:data}); 
      }
      if (err) {
          return  res.status(400).send({success:false, msg:'data not found'});
      }
  })
}

The song controller, add songs

const { Songs } = require("../models/song.model");

exports.addSong = async function (req, res) {
  const data = req.body;
  try {
    const song = new Songs(data);
    song.save();
    return res.status(201).json(song);
  } catch (error) {
    console.log(error.message);
    return res.status(500).json({ msg: error });
  }
};

Get songs

exports.getSongs = async function (req, res) {
  try {
    const option={
      sort:{
        createdAt:1,
      },
    }
    data = await Songs.find(option);
    return res.status(200).json(data);
  } catch (error) {
    console.log(error.message);
    return res.status(500).json({ msg: error });
  }
};

Delete a song, by adding the song id as params

exports.deleteSongs = async function (req, res) {
  try {
    const song = await Songs.findByIdAndDelete({ _id: req.params.id });
    if (!song) {
      return res
        .status(404)
        .json({ msg: `No song with id ${req.params.id}` });
    } else {
      return res
        .status(200)
        .json({ msg: "song Deleted Successfully", data: null });
    }
  } catch (error) {
    console.log(error);
  }
};

Next is the route folder,

User route

const router = require("express").Router();
const {login,getUser,getUsers,deleteUser,updateRole} = require("../controllers/user.controller")

router.get("/login",login);
router.get("/get",getUsers);
router.get("/get/:id",getUser);
router.delete("/delete/:id",deleteUser);
router.put("/updateRole/:id",updateRole);

module.exports = router;

Song route

const router = require("express").Router();
const {getSong,getSongs,addSong,deleteSongs,updateSong} = require("../controllers/song.controller");
const {songValidation} = require("../validation/validation")

router.post("/save",songValidation,addSong);
router.get("/get",getSongs);
router.get("/get/:id",getSong);
router.delete("/delete/:id",deleteSongs);
router.put("/update/:id",updateSong);

module.exports = router;

Lastly on the server side is the index.js

const express = require("express");
const cors = require("cors");
const { connectDatabase } = require("./src/config/database");
const userRoute=require("./src/routes/user.route");
const albumRoutes=require("./src/routes/album.route");
const artistRoutes=require("./src/routes/artist.route");
const songRoutes=require("./src/routes/song.route");

const app = express();
connectDatabase(app);
app.use(cors({origin:true}));
app.use(express.json());

app.use('/api/users',userRoute)
app.use('/api/album',albumRoutes)
app.use('/api/artist',artistRoutes)
app.use('/api/song',songRoutes)

app.get("/", (req, res) => {
  res.status(200).json({ msg: "Welcome to the api for the music app" });
});

app.all("*", (req, res) => {
  res.send({
    status: false,
    messsage: "Oops! you've hit an invalid route.",
  });
});

Frontend Development

The frontend of Itunez is built using React.js, a popular JavaScript library for building user interfaces, Tailwindcss and Redux for managing states. The code is organized into several folders within the "src" directory. These folders include API, assets, cards, config, features, pages, and utils. The API folder handles communication with the backend, enabling the retrieval of music data for display on the homepage. Assets contain static files such as images and CSS stylesheets. Card and page folders define reusable components that represent individual music tracks and album pages, respectively. The config folder contains configuration files for the Firebase storage, while the features folders manage the react-redux files which include the slice and store for managing states and contexts. utils folders provide additional functionality and utility functions.

Let's begin with the Api folder, in the index.js all the API calls are made here and exported for consumption using Axios

import axios from 'axios';

const baseUrl = process.env.REACT_APP_BASE_URL;

export const validateUser= async (token)=>{
    try {
        const res = await axios.get(`${baseUrl}api/users/login`,{
            headers:{
                Authorization:'Bearer ' + token,
            }
        })
        return res.data;
    } catch (error) {
        return null;
    }
}

export const getAllUsers= async ()=>{
    try {
        const res = await axios.get(`${baseUrl}api/users/get`)
        return res.data;
    } catch (error) {
        return null;
    }
}

others..

export const getAllSongs= async ()=>{
    try {
        const res = await axios.get(`${baseUrl}api/song/get`)
        return res.data;
    } catch (error) {
        return null;
    }
}

export const changeUserRole= async (userId,role)=>{
    try {
        const res = await axios.put(`${baseUrl}api/users/updateRole/${userId}`,{role})
        return res.data;
    } catch (error) {
        return null;
    }
}

export const deleteUser= async (userId)=>{
    try {
        const res = await axios.delete(`${baseUrl}api/users/delete/${userId}`)
        return res.data;
    } catch (error) {
        return null;
    }
}

The frontend also requires the firebase.js to initialize the app

import {getApp,getApps,initializeApp} from 'firebase/app'
import {getStorage} from 'firebase/storage'

const firebaseConfig = {
  apiKey: process.env.REACT_APP_API_KEY,
  authDomain: process.env.REACT_APP_AUTH_DOMAIN,
  projectId: process.env.REACT_APP_PROJECT_ID,
  storageBucket: process.env.REACT_APP_STORAGE,
  messagingSenderId: process.env.REACT_APP_MESSAGING_ID,
  appId: process.env.REACT_APP_APP_ID
};


const app = getApps.length > 0 ? getApp() : initializeApp(firebaseConfig);
const storage = getStorage(app);

export {app,storage};

Lastly, the redux, reduxjs/toolkit is required

Features folder/userslice

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';

const url = process.env.REACT_APP_BASE_URL;

const initialState = {
  user: [],
  allUsers:[],
  isLoading:false,
  alertType:null,
};

export const getAllUsers = createAsyncThunk('user/getAllUsers', async (thunkAPI) => {
  try {
    const res = await axios.get(`${url}api/users/get`)
    return res.data;

  } catch (error) {
    return thunkAPI.rejectWithValue('something went wrong');
  }
}
);


export const userValidator = createAsyncThunk('user/uservalidator', async (token, thunkAPI) => {
    try {
      const res = await axios.get(`${url}api/users/login`,{
        headers:{
            Authorization:'Bearer ' + token,
        }
    })
    return res.data;

    } catch (error) {
      return thunkAPI.rejectWithValue('something went wrong');
    }
  }
);

const userSlice = createSlice({
  name: 'user',
  initialState,
  reducers: {
    nullUser: (state) => {
      state.user = null;
    },
    namedUser: (state,{payload}) => {
      state.user = payload;
    },

  },
  extraReducers: (builder) => {
    builder
      .addCase(userValidator.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(userValidator.fulfilled, (state, action) => {
        // console.log(action);
        state.isLoading = false;
        state.user = action.payload;
      })
      .addCase(userValidator.rejected, (state, action) => {
        console.log(action);
        state.isLoading = false;
      }).addCase(getAllUsers.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(getAllUsers.fulfilled, (state, action) => {
        // console.log(action);
        state.isLoading = false;
        state.allUsers = action.payload;
      })
      .addCase(getAllUsers.rejected, (state, action) => {
        console.log(action);
        state.isLoading = false;
      })
  },
});

export const { positiveAlert,negativeAlert,nullAlert,openPlayer,closePlayer,zeroIndex,increaseIndex,decreaseIndex,setIndex,nullUser,namedUser} =
  userSlice.actions;

export default userSlice.reducer;

songslice

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import axios from 'axios';


const url = process.env.REACT_APP_BASE_URL;

const initialState = {
  allSongs: [],
  isLoading:false,
  filterTerm:"all",
  artistFilter:null,
  languageFilter:null,
  albumFilter:null,
};

export const getAllSongs = createAsyncThunk('songs/getAllSongs', async (thunkAPI) => {
  try {
    const res = await axios.get(`${url}api/song/get`)
    return res.data;

  } catch (error) {
    return thunkAPI.rejectWithValue('something went wrong');
  }
}
);

const songsSlice = createSlice({
  name: 'songs',
  initialState,
  reducers: {
    namedFilter: (state,{payload}) => {
      state.filterTerm = payload;
    },
    nullFilter: (state) => {
      state.filterTerm = "Categories";
    },
    nullArtistFilter: (state) => {
      state.artistFilter = "Artist";
    },
    namedArtistFilter: (state,{payload}) => {
      state.artistFilter = payload;
    },

  },
  extraReducers: (builder) => {
    builder
      .addCase(getAllSongs.pending, (state) => {
        state.isLoading = true;
      })
      .addCase(getAllSongs.fulfilled, (state, action) => {
        // console.log(action);
        state.isLoading = false;
        state.allSongs = action.payload;
      })
      .addCase(getAllSongs.rejected, (state, action) => {
        console.log(action);
        state.isLoading = false;
      })
  },
});

export const { namedFilter,nullFilter,nullArtistFilter,namedArtistFilter,nullLanguageFilter,namedLanguageFilter,namedalbumFilter,nullalbumFilter} = songsSlice.actions;

export default songsSlice.reducer;

The store

import { configureStore } from '@reduxjs/toolkit';
import userReducer from './features/users/userSlices';
import songsReducer from './features/songs/songSlice';
export const store = configureStore({
  reducer: {
    user: userReducer,
    songs:songsReducer,
  },
});

Conclusion

The Itunez music player app demonstrates the use of Node.js and React.js to build a full-stack web application. With its backend implemented in Node.js and MongoDB, and frontend developed using React.js, Itunez provides a seamless and enjoyable music listening experience for users. The integration of Firebase for authentication and storage enhances the app's security and functionality. By utilizing platforms like Render and Vercel for hosting, the Itunez app ensures a reliable and scalable deployment.

Frontend repo: https://github.com/Rahdeg/mymusic

Backend repo: https://github.com/Rahdeg/musicback

Itunez live app: https://itunez.vercel.app/

To explore more projects, visit my portfolio at https://potfolio-b5fc1.web.app/.