Handling Uploads On The Back-End

Web clients submit uploaded files through a form encoded as multipart/form-data. Multer is an Express.js and Node.js middleware built to handle that encoding. Digital media assets such as audio and video add server-side complications — networking, storage, and the asynchronous nature of file handling — but Multer plus Express.js simplifies the back-end workflow, while Nuxt.js (the Vue framework) covers the front-end interactions.

The stack for this build assumes familiarity with HTML, CSS, and JavaScript (ES6+), a machine with Node.js, npm, and MongoDB installed, a code editor, and basic Express.js knowledge.

Project Bootstrap

Create a directory for the project, navigate into it, and run npm init -y to generate the package.json that manages dependencies.

mkdir serverside && cd serverside
npm init -y

Install multer, express, and the remaining dependencies needed to bootstrap an Express.js app.

npm install express multer nodemon mongoose cors morgan body-parser --save

Create an index.js file:

touch index.js

Inside it, initialize the modules, create the Express.js app, and create a server for connecting to browsers:

const express = require("express");
const PORT = process.env.PORT || 4000;
const morgan = require("morgan");
const cors = require("cors");
const bodyParser = require("body-parser");
const mongoose = require("mongoose");
const config = require("./config/db");
const app = express();
//configure database and mongoose
mongoose.set("useCreateIndex", true);
mongoose
  .connect(config.database, { useNewUrlParser: true })
  .then(() => {
    console.log("Database is connected");
  })
  .catch(err => {
    console.log({ database_error: err });
  });
// db configuaration ends here
//registering cors
app.use(cors());
//configure body parser
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());
//configure body-parser ends here
app.use(morgan("dev")); // configire morgan
// define first route
app.get("/", (req, res) => {
  res.json("Hola MEVN devs...Assemble");
});
app.listen(PORT, () => {
  console.log(`App is running on ${PORT}`);
});

Express.js, body-parser, morgan, mongoose, and cors are pulled into the project, along with a port for the application. The Express instance is stored in a variable called app, which configures middleware — as with the cors middleware — and sets up the root route that runs on the defined port.

Database And Upload Configuration

Create a /config folder for the database config and the multer config:

mkdir config and cd config
touch multer.js && touch db.js

Then open config/db.js and add the code that configures the database:

module.exports = {
  database: "mongodb://localhost:27017/",
  secret: "password"
};

This is an object holding the database URL and the database secret. Running nodemon and visiting localhost:4000 should produce this message in the browser:

"Hola MEVN devs...Assemble"

The terminal should look like this:

Running Nodemon using Terminal
Terminal preview (Large preview)

Model, Routes, And Controllers

Set up the file structure by typing the following:

mkdir api && cd api
mkdir model && cd model && touch Music.js
cd ..
mkdir controller && cd controller && touch musicController.js
cd ..
mkdir routes && cd routes && touch music.js

The mkdir command creates the api directory, and cd moves into it. The touch command creates a new file inside a directory; cd also moves back out of a directory.

In api/model/Music.js, create a music schema. A model is a class used to construct documents; here each document is a piece of music with the properties and behaviors declared in the schema:

let mongoose = require("mongoose");
let musicSchema = mongoose.Schema({
  title: {
    type: String,
    required: true
  },
  music: {
    type: Object,
    required: true
  },
  artist: {
    type: String,
    required: true
  },
  created: {
    type: Date,
    default: Date.now()
  }
});
let Music = mongoose.model("Music", musicSchema);
module.exports = Music;

Next, configure Multer in config/multer:

let multer = require("multer");
const path = require("path");
const storage = multer.diskStorage({
  destination: (req, res, cb) => {
    cb(null, "./uploads");
  },
  filename: (req, file, cb) => {
    cb(null, new Date().toISOString() + file.originalname);
  }
});
const fileFilter = (req, file, cb) => {
  if (
     file.mimetype === "audio/mpeg" ||
     file.mimetype === "audio/wave" ||
     file.mimetype === "audio/wav" ||
     file.mimetype === "audio/mp3"
  ) {
    cb(null, true);
  } else {
    cb(null, false);
  }
};
exports.upload = multer({
  storage: storage,
  limits: {
    fileSize: 1024 * 1024 * 5
  },
  fileFilter: fileFilter
});

The multer.js file first sets up a folder where uploaded music files will go. That folder has to be made static by defining it in index.js:

app.use('/uploads', express.static('uploads'));

A simple validator then checks the file mimetype before upload. The multer instance is defined with the storage location, per-file limits, and the validator.

Routes And Controllers

The endpoints we will create are listed below:

HTTP POST /musicAdd new music
HTTP GET /musicGet all music
HTTP DELETE /music/:blogIdDelete a music

Start with the blog route in api/routes/music.js:

const express = require("express");
const router = express.Router();
const musicController = require("../controller/musicController");
const upload = require("../../config/multer");
router.get("/",  musicController.getAllMusics);
router.post("/", upload.upload.single("music"), musicController.addNewMusic);
router.delete("/:musicId", musicController.deleteMusic);
module.exports = router;

Note: any get request to /music calls the getAllMusic function located in the controllers file.

In api/controllers/musicController, define the controllers. The first function gets all the music in the database via the mongoose db.collection.find method, which returns every item in the collection. A second function creates a new piece of music: a new music instance is created with the new keyword, the music object is defined, and the mongoose save method writes it to the database. Deleting a piece of music uses the mongoose remove method, passing the music ID as a parameter — mongoose locates the item with that ID in the music collection and removes it.

let mongoose = require("mongoose");
const Music = require("../model/Music");
exports.getAllMusics = async (req, res) => {
  try {
    let music = await Music.find();
    res.status(200).json(music);
  } catch (err) {
    res.status(500).json(err);
  }
};
exports.addNewMusic = async (req, res) => {
  try {
    const music = new Music({
      title:req.body.title,
      artist:req.body.artist,
      music:req.file
    });
    
    let newMusic = await music.save();
    res.status(200).json({ data: newMusic });
  } catch (err) {
    res.status(500).json({ error: err });
  }
};
exports.deleteMusic = async (req, res) => {
  try {
    const id = req.params.musicId;
    let result = await Music.remove({ _id: id });
    res.status(200).json(result);
  } catch (err) {
    res.status(500).json(err);
  }
};

Finally, register the music routes in index.js so the routes can be tested:

const userRoutes = require("./api/user/route/user"); //bring in our user routes
app.use("/user", userRoutes);

Exercising The Endpoints

POSTMAN is used to test the endpoints.

To test Add Music, pick the request method from the methods drop-down, type the endpoint URL, then click the body tab and choose how the data is sent — here, the form-data method. Select form-data and set up the model keys, giving each key a value.

Testing Adding new music API using Postman
Testing Adding new music API in Postman dashboard (Large preview)

Click ‘Send’ to make the request.

To list all the music in the database, type the endpoint URL into the URL section and click ‘Send’.

Testing Listing API using Postman
Testing Listing API in Postman dashboard (Large preview)

To delete a piece of music, pass the music id as a parameter.

Testing Delete API using Postman
Testing Delete API Postman dashboard (Large preview)

Building The Frontend

The frontend uses the Vue framework Nuxt.js.

“Nuxt is a progressive framework based on Vue.js to create modern web applications. It is based on Vue.js official libraries (vue, vue-router and vuex) and powerful development tools (webpack, Babel and PostCSS).”

— NuxtJS Guide

Create a new Nuxt.js application from the terminal, with musicapp as the app name:

$ npx create-nuxt-app musicapp

The installation asks a few questions about the project setup:

Project namemusicapp
project descriptionA Simple music manager app
Author name<your name>
Package managernpm
UI frameworkBootstrap vue
custom ui frameworknone
Nuxt modulesAxios,pwa (use the spacebar on your keyboard to select items)
Linting toolPrettier
test frameworkNone
Rendering ModeUniversal (SSR)
development toolJsonconfig.json

Once the selections are made, wait for the project to be set up, then move into the /project folder and serve it:

cd musicapp && npm run dev

Open the project in a code editor and in the browser at localhost:3000.

Preview Of Nuxt.js project
Nuxt.js Project Preview (Large preview)

Pointing Axios At The API

axios makes the HTTP requests to the back-end server. It is already installed in the project, so the only step is configuring the baseURL to the backend server. Open the nuxt.config.js file in the root directory and add the baseURL in the axios object.

axios: {
  baseURL:'https://localhost:4000'
},

Assembling The Music Manager

Clean up the UI first. Open pages/index.vue and replace all of the code in it with the following:

<template>
<div>Hello</div>
</template>

The browser should now show only a “Hello”.

In the root directory, create a /partials folder, then create a navbar.vue file inside it with the code below:


<template>
  <header>
    <nav class="navbar navbar-expand-lg navbar-light bg-info">
      <div class="container">
        <a class="navbar-brand" href="#">Music App</a>
        <button
          class="navbar-toggler"
          type="button"
         
         
          aria-controls="navbarNav"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse justify-content-end" id="navbarNav">
          <ul class="navbar-nav">
            <li class="nav-item active">
              <a class="nav-link" href="#">Player</a>
            </li>
            <li class="nav-item">
              <a class="nav-link" href="#">Manager</a>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>
</template>
<style scoped>
.nav-link,
.navbar-brand {
  color: #ffff !important;
}
</style>

Note: this component is used to navigate between pages in the application. It is a simple component built from the Bootstrap navbar.

Next, define a custom layout for the application. Open the /layouts folder and replace the code in default.vue with the following:

<template>
  <div>
    <navbar />
    <nuxt />
  </div>
</template>
<script>
import navbar from '@/partial/navbar'
export default {
  components: {
    navbar
  }
}
</script>

Importing the navbar into this layout means every page in the application carries that navbar component, which also serves as the mount point for all other components.

The browser should now show the navbar:

Nuxt.js Navbar component after modification
Nuxt.js Navbar component (Large preview)

Now set up the manager UI: create a /manager folder inside the components folder and add a file named manager.vue.

<template>
  <section class="mt-5">
    <div class="container mb-4">
      <div class="row">
        <div class="col-md-12">
          <div class="card">
            <div class="card-body">
              <div class="card-title mb-4">
                <h4>Add Music</h4>
              </div>
              <form>
                <div class="form-group">
                  <label for="title">Title</label>
                  <input type="text" class="form-control" />
                </div>
                <div class="form-group">
                  <label for="artist">Artist</label>
                  <input type="text" class="form-control" />
                </div>
                <div class="form-group">
                  <label for="artist">Music</label>
                  <div class="custom-file">
                    <input type="file" class="custom-file-input" id="customFile" />
                    <label class="custom-file-label" for="customFile">Choose file</label>
                  </div>
                </div>
                <div class="form-group">
                  <button class="btn btn-primary">Submit</button>
                </div>
              </form>
            </div>
          </div>
        </div>
      </div>
    </div>
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <div class="card bg-light p-1 showdow-sm">
            <div class="card-title">
              <button class="btn btn-info m-3">Add Music</button>
            </div>
            <div class="card-body">
              <table class="table">
                <thead>
                  <tr>
                    <th scope="col">#</th>
                    <th scope="col">Title</th>
                    <th scope="col">Artist</th>
                    <th scope="col">Date created</th>
                    <th scope="col">Action</th>
                  </tr>
                </thead>
                <tbody>
                  <tr>
                    <td>1</td>
                    <td>Demo Title</td>
                    <td>Wisdom.vue</td>
                    <td>12/23/13</td>
                    <td>
                      <button class="btn btn-info">Delete</button>
                    </td>
                  </tr>
                </tbody>
              </table>
            </div>
          </div>
        </div>
      </div>
    </div>
  </section>
</template>

Note: this is a simple Bootstrap template for adding music to the application. The form defines a table template that lists all the music in the database.

The component then has to be registered in the /pages folder to initialize routing. Nuxt.js has no ‘router.js’ file as Vue.js does; it routes from the pages folder. To register the component, create a /manager folder inside the /pages folder, create an index.vue file, and place the following code in it:

<template>
  <div>
    <manager />
  </div>
</template>
<script>
import manager from '@/components/manager/manager'
export default {
  components: {
    manager
  }
}
</script>

This is the component that renders in the pages route. Navigating to /manager in the browser should show it.

UI of music Manager
Music manager UI (Large preview)

Fetching And Rendering The Library

To fetch all of the music, define a function and register it in the created life cycle hook so it is called whenever the component is created. Start by creating a variable in the vue instance to hold all of the music:

allmusic = [];
musicLoading: false,

Then define a getAllMusics function:

async getAllMusics() {
    this.musicLoading = true
    try {
      let data = await this.$axios.$get('/music')
      this.allmusic = data
      this.musicLoading = false
    } catch (err) {
      this.musicLoading = false
      swal('Error', 'Error Fetting Musics', 'error')
    }
  }

Register it within the created life cycle hook:

created() {
    this.getAllMusics()
  }

Finally, output the songs in the table built earlier by looping through the response from the backend, listing every piece of music received back from the database:

<table class="table">
              <thead>
                <tr>
                  <th scope="col">#</th>
                  <th scope="col">Title</th>
                  <th scope="col">Artist</th>
                  <th scope="col">Date created</th>
                  <th scope="col">Action</th>
                </tr>
              </thead>
              <div
                v-if="musicLoading"
                class="spinner-border"
                style="width: 3rem; height: 3rem;"
                role="status"
              >
                <span class="sr-only">Loading...</span>
              </div>
              <tbody v-else>
                <tr v-for="(music, index) in allmusic" :key="index">
                  <td>{{ index + 1 }}</td>
                  <td>{{ music.title }}</td>
                  <td>{{ music.artist }}</td>
                  <td>{{ music.created }}</td>
                  <td>
                    <button class="btn btn-info" @click="deleteMusic(music._id)">Delete</button>
                  </td>
                </tr>
              </tbody>
            </table>

Wiring the Add Form to the API

Creating a track means posting its details to the Express service. Start on the form: bind the file field with a v-model and attach a submit listener to the form itself.

<form @submit.prevent="addNewMusic">
            <div class="form-group">
              <label for="title">Title</label>
              <input type="text" v-model="musicDetails.title" class="form-control" />
            </div>
            <div class="form-group">
              <label for="artist">Artist</label>
              <input type="text" v-model="musicDetails.artist" class="form-control" />
            </div>
            <div class="form-group">
              <label for="artist">Music</label>
              <div class="custom-file">
                <input
                  type="file"
                  id="customFile"
                  ref="file"
                  v-on:change="handleFileUpload()"
                  class="custom-file-input"
                />
                <label class="custom-file-label" for="customFile">Choose file</label>
              </div>
            </div>
            <div class="form-group">
               <button class="btn btn-primary" :disabled="isDisabled">
                <span
                  class="spinner-border spinner-border-sm"
                  v-if="addLoading"
                  role="status"
                  aria-hidden="true"
                ></span>Submit
              </button>
            </div>
          </form>
<script>
export default {
  data() {
    return {
      musicDetails: {
        title: '',
        artist: '',
        music: ''
      },
      allmusic = [],
        musicLoading: false,
      isValid: false;
      addLoading: false,
    }
  },
  computed: {
    isDisabled: function() {
      if (
        this.musicDetails.title === '' ||
        this.musicDetails.artist === '' ||
        this.musicDetails.music === ''
      ) {
        return !this.isValid
      }
    }
  },
  methods: {
    handleFileUpload() {
      this.musicDetails.music = this.$refs.file.files[0]
      console.log(this.musicDetails.music.type)
    },
    addNewMusic() {
      let types = /(\.|\/)(mp3|mp4)$/i
      if (
        types.test(this.musicDetails.music.type) ||
        types.test(this.musicDetails.music.name)
      ) {
        console.log('erjkb')
      } else {
        alert('Invalid file type')
        return !this.isValid
      }
    }
  }
}
</script>

The submit handler sends the request, and a small validator restricts uploads to .mp3 and .mp4 files. A computed property keeps the input from being submitted empty.

Before writing addMusic, install the modal library:

npm i sweetalert

Add a plugin file at /plugins/sweetalert.js:

import Vue from 'vue';
import swal from 'sweetalert';

Vue.prototype.$swal = swal;

and register it in the plugins array of nuxt.config.js:

plugins: [
    {
      src: '~/plugins/sweetalert'
    }
  ],

Now the request itself:

addNewMusic() {
    let types = /(\.|\/)(mp3|mp4)$/i
    if (
      types.test(this.musicDetails.music.type) ||
      types.test(this.musicDetails.music.name)
    ) {
      let formData = new FormData()
      formData.append('title', this.musicDetails.title)
      formData.append('artist', this.musicDetails.artist)
      formData.append('music', this.musicDetails.music)
      this.addLoading = true
      this.$axios
        .$post('/music', formData)
        .then(response => {
          console.log(response)
          this.addLoading = false
          this.musicDetails = {}
          this.getAllMusics() // we will create this function later
          swal('Success', 'New Music Added', 'success')
        })
        .catch(err => {
          this.addLoading = false
          swal('Error', 'Something Went wrong', 'error')
          console.log(err)
        })
    } else {
      swal('Error', 'Invalid file type', 'error')
      return !this.isValid
    }
  },

Showing and hiding the form

The add form should only appear on demand. Edit the ‘Add Music’ button in the music table:

<button
    class="btn btn-info m-3"
    @click="initForm">
    {{addState?"Cancel":"Add New Music"}}
</button>

Hold the form’s visibility in a data property:

addState: false

Define initForm:

initForm() {
    this.addState = !this.addState
  },

and gate the wrapper div with v-if="addState":

<div class="card" v-if="addState">

Removing Tracks

Deletion goes through the delete endpoint with the music ID as a param. Add a click handler to the ‘Delete’ button:

<button class="btn btn-info" @click="deleteMusic(music._id)">Delete</button>

deleteMusic receives the ID as a parameter, appends it to the request URL, and the server removes exactly that record from the database.

deleteMusic(id) {
    swal({
      title: 'Are you sure?',
      text: 'Once deleted, you will not be able to recover this Music!',
      icon: 'warning',
      buttons: true,
      dangerMode: true
    }).then(willDelete => {
      if (willDelete) {
        this.$axios
          .$delete('/music/' + id)
          .then(response => {
            this.getAllMusics()
            swal('Poof! Your Music file has been deleted!', {
              icon: 'success'
            })
          })
          .catch(err => {
            swal('Error', 'Somethimg went wrong', 'error')
          })
      } else {
        swal('Your Music file is safe!')
      }
    })
  }

The manager is finished at this point; the remainder of the work is the player.

Scaffolding the Player

Create a /player folder inside components and a player.vue file in it:

<template>
  <section>
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <h3 class="text-center">Player</h3>
        </div>
      </div>
    </div>
  </section>
</template>
<script>
export default {
  data() {
    return {}
  }
}
</script>
<style  scoped>
</style>

Import that component into pages/index.vue:

<template>
  <div>
    <player />
  </div>
</template>
<script>
import player from '@/components/player/player'
export default {
  components: {
    player
  }
}
</script>

Routing between pages uses nuxt-link, which takes the target page as its destination. Update partials/navbar accordingly:

<template>
  <header>
    <nav class="navbar navbar-expand-lg navbar-light bg-info">
      <div class="container">
        <nuxt-link to="/" class="navbar-brand">Music App</nuxt-link>
        <button
          class="navbar-toggler"
          type="button"
         
         
          aria-controls="navbarNav"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span class="navbar-toggler-icon"></span>
        </button>
        <div class="collapse navbar-collapse justify-content-end" id="navbarNav">
          <ul class="navbar-nav">
            <li class="nav-item active">
              <nuxt-link to="/" class="nav-link">Player</nuxt-link>
            </li>
            <li class="nav-item">
              <nuxt-link to="/manager" class="nav-link">Manager</nuxt-link>
            </li>
          </ul>
        </div>
      </div>
    </nav>
  </header>
</template>
<style scoped>
.nav-link,
.navbar-brand {
  color: #ffff !important;
}
</style>

Teaching Webpack about audio

Webpack’s default file-loader handles audio only if you configure it to. Amend the build object in nuxt.config.js:

build: {
    extend(config, ctx) {
      config.module.rules.push({
        test: /\.(ogg|mp3|mp4|wav|mpe?g)$/i,
        loader: 'file-loader',
        options: {
          name: '\[path\][name].[ext]'
        }
      })
    }
  }

Then write a function that fetches every song and plays the first entry of allMusic through the Audio constructor:

<template>
  <section v-if="allMusic">
    <div class="container">
      <div class="row">
        <div class="col-md-12">
          <h3 class="text-center">Player</h3>
        </div>
      </div>
      <div class="row">
        <div class="col-md-6">
          <span>{{this.current.title}} - {{this.current.artist}}</span>
        </div>
      </div>
    </div>
  </section>
</template>
<script>
export default {
  data() {
    return {
      current: {
        title: '',
        artist: ''
      },
      song: true,
      isplaying: false,
      allMusic: null,
      index: 0,
      player: ''
    }
  },
  methods: {
     async initPlayer() {
      if (this.allMusic !== []) {
        this.current = await this.allMusic[this.index]
        this.player.src = `https://localhost:4000/${this.current.music.path}`
      } else {
        this.song = true
      }
    },
      async getAllSongs() {
        try {
        let response = await this.$axios.$get('/music')
        console.log(response)
        if (response === []) {
          this.song = true
          this.current = null
        } else {
          this.song = false
          this.allMusic = response
        }
        await this.initPlayer()
      } catch (err) {
        this.current = null
        console.log(err)
      }
    }
  },
  created() {
 if (process.client) {
      this.player = new Audio()
    }
    this.getAllSongs()
  }
}
</script>
<style  scoped>
</style>

The file streams in the background and starts playing:

UI of Music player
Music player UI (Large preview)

Commenting out await player.play() inside initPlayer prevents playback.

The Player Interface

Replace the template in player.vue with the player UI:

<template>
  <section v-if="allMusic">
    <div class="container">
      <div class="row mb-5">
        <div class="col-md-12">
          <h3 class="text-center">Player</h3>
        </div>
      </div>
      <div class="row mt-5">
        <div class="col-md-6">
          <img
            src="https://images.pexels.com/photos/3624281/pexels-photo-3624281.jpeg?auto=compress&cs=tinysrgb&dpr=1&w=500"
            class="image"
          />
          <div class="card player_card">
            <div class="card-body">
              <h6 class="card-title">
                <b>{{this.current.title}} - {{this.current.artist}}</b>
              </h6>
              <div>
                <i class="fas fa-backward control mr-4"></i>
                <i class="fas fa-play play"></i>
                <i class="fas fa-pause play"></i>
                <i class="fas fa-forward control ml-4"></i>
              </div>
            </div>
          </div>
        </div>
        <div class="col-md-6">
          <div class="card shadow">
            <table class="table">
              <thead>
                <tr>
                  <th scope="col">#</th>
                  <th scope="col">Title</th>
                  <th scope="col">Artist</th>
                  <th scope="col">Action</th>
                </tr>
              </thead>
              <tbody>
                <tr>
                  <th scope="row">1</th>
                  <td>Mark</td>
                  <td>Otto</td>
                  <td>
                    <button class="btn btn-primary">Play</button>
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>
      </div>
    </div>
  </section>
</template>

and add matching rules to the style block:

<style  scoped>
.image {
  border-radius: 5px !important;
  position: relative;
  height: 300px;
  width: 100%;
}
.player_card {
  text-align: center;
  bottom: 20px;
  margin: 0px 40px;
}
.text-muted {
  font-size: 15px;
}
.play {
  font-size: 40px;
}
.control {
  font-size: 25px;
}
</style>
Music player final UI
Final UI of music player (Large preview)

Play, Pause, and Track Switching

Showing descriptions and toggling icons

The table should surface each track’s description, so swap it for this version:

<table class="table">
              <thead>
                <tr>
                  <th scope="col">#</th>
                  <th scope="col">Title</th>
                  <th scope="col">Artist</th>
                  <th scope="col">Action</th>
                </tr>
              </thead>
              <tbody>
                <tr v-for="(music,index) in allMusic" :key="index">
                  <th scope="row">{{index+1}}</th>
                  <td>{{music.title}}</td>
                  <td>{{music.artist}}</td>
                  <td>
                    <button class="btn btn-primary">Play</button>
                  </td>
                </tr>
              </tbody>
            </table>

Play and pause icons must never render together: pause while audio plays, play while it is stopped. Add an isPlaying state initialised to false and switch the icons off it.

isplaying:false
<i class="fas fa-play play" v-if="!isplaying" @click="play"></i>
<i class="fas fa-pause play" v-else></i>

Define the play method:

play(song) {
      console.log(song)
      if (song) {
        this.current = song
        this.player.src = `https://localhost:4000/${this.current.music.path}`
      }
      this.player.play()
      this.isplaying = true
    },

It takes the selected song as its parameter, creates an Audio() instance, and bails out if that song is null. Otherwise it assigns the song to this.current, starts the audio instance, and sets isPlaying to true.

Pausing

Pausing uses the Audio instance’s pause method, triggered from a click on the pause icon:

<i class="fas fa-pause play" @click="pause" v-else></i>
pause() {
      this.player.pause()
      this.isplaying = false
    },

Starting a track from the list

Bind a click that passes the row’s song into play:

<button class="btn btn-primary" @click="play(music)">Play</button>

Next track

Next advances the index by one, again via a click handler on the icon:

@click="next"
next() {
      this.index++
      if (this.index > this.allMusic.length - 1) {
        this.index = 0
      }
       this.current = this.allMusic[this.index]
      this.play(this.current)
    },

The conditional in that method restarts the list once the final song has played.

Previous track

Previous mirrors next:

@click="prev"
prev() {
      this.index--
      if (this.index < 0) {
        this.index = this.allMusic.length - 1
      }
      this.current = this.allMusic[this.index]
      this.play(this.current)
    },

The application is complete.

Takeaways

The build combined Nuxt.js on the client with Express.js on the server. Multer handled the file uploads, Mongoose mediated the database work, and Nuxt’s conventions supplied the fast, structured client experience. Nuxt’s route management in particular pushed the app toward a cleaner structure than other frameworks.