How to Create a Public File Sharing Service with Vue.js and Node.js

How to Create a Public File Sharing Service with Vue.js and Node.js

This tutorial explores how to create a public file sharing service with link shortening and file type restriction features with Vue.js, Node.js.

This tutorial explores how to create a public file sharing service with link shortening and file type restriction features with Vue.js, Node.js.

File upload plays an integral part in many web applications. It is used in programs such as email clients, chat applications, commenting systems, among others.

Before JavaScript frameworks dominated web development, file upload systems were similar. These systems usually comprise a form with a file input. After the form is submitted, the backend receives the files, stores them and redirects the user’s page. With the popularity of JavaScript frameworks today, the situation is different. File uploads nowadays can have several features. Some of these are AJAX submissions, progress bars, pause and continue features.

The finished code for this tutorial can be found at these Github repositories:

Application Architecture

In this article, we will be building a public file upload and sharing service. It will have a Node.js-powered backend and a Vue.js-powered frontend. The service will be used anonymously and won’t have any authenticated users. We will submit the file through AJAX and store it in the backend filesystem.

The file meta-data information will be stored in a MongoDB database. All the uploaded files will be listed on the homepage of the application. Upon clicking the name of the file, we will make a request to the backend to download the file. We will also be able to delete files.

We will include a URL-shortener feature in the application to make it easier to share links. This will be achieved by generating a unique hash for each uploaded file. We will also add a mechanism to restrict which file types can be uploaded with the application.

Install and Configure Packages

Before we can install any of the packages, we will need a few things. We must have Node.js installed on our system along with the MongoDB database.

Now that we have a clear understanding of the application requirements, let’s start building it. The application will have a separate backend and frontend. Each will be in a separate folder. Create two folders named client and server in the same directory. Move to the server folder and initialize a new Node.js application with:

npm init  


Accept the default values when prompted. Next, move out of the server folder to the level where both folders reside. Initialize a new Vue.js application with:

vue init webpack client  


Accept the default values as well. When asked to install the router plugin, select yes. Now that we have scaffolded the frontend and backend, let’s install the required packages, starting with the frontend. Move into the client folder and install the Axios.js package using the following command:

npm install --save axios  


Navigate again to the server folder and, install the packages:

npm install --save btoa body-parser express mongoose multer  


Let’s outline the purpose of each of the packages:

List Uploaded Files

Now that the packages are installed, we will list the files from the backend. For now, we do not have any uploaded files yet but we will get to that later. In the server folder, there should be an index.js file. If it is not present, create it. In there, import several libraries by pasting the following:

const bodyParser = require('body-parser');  
const express = require('express');  
const app = express();

app.listen(3000, () => {  
  console.log('Server started on port : ' + 3000);
});


Start the Node.js server using:

node index.js  


There should be a message in the console without any errors. The message should read:

Server started on port: 3000  


Next, create a file in models/file.js. In there paste in the following:

const mongoose = require('mongoose');  
const Schema = mongoose.Schema;

let FileSchema = new Schema({  
  name: { type: String, required: true, max: 100 },
  encodedName: { type: String, required: false, max: 100, default: null }
});

module.exports = mongoose.model('file', FileSchema, 'files');  


Here, we are using Mongoose.js to create the model to represent a single file. This is what we will use to query the database for uploaded files. To use it, we only have to import the exported module from the file.

Next, let’s create a service file. This is where our logic for querying the database will be. It will also contain the connection information for the MongoDB database. Still in the server directory, create a file in services/file.service.js. In there, paste the following:

const mongoose = require('mongoose');  
const File = require('../models/file');  
const multer = require('multer');  
const async = require('async')  
const fs = require('fs')  
const path = require('path')  
const btoa = require('btoa')  


In the lines above, we are requiring several libraries. We have also included extra native Node.js libraries: async, fs and path. The function for async is to perform many asynchronous operations. When all operations are complete, we have one success callback. fs is used to create, delete and manipulate local files. Finally, we use path to create folder paths in a safe way depending on the environment.

Let’s now connect to the database and write our method to fetch all uploaded files information in the database. Note that we will only store file information in the database. The physical file itself will be in a folder somewhere in the server. In the same file, paste in the following:

const fileConfig = require('../config/file.config')  
const mongoDB = fileConfig.dbConnection;  
mongoose.connect(mongoDB, { useNewUrlParser: true });  
mongoose.Promise = global.Promise;  


This connects to our database and requires the configuration information for our file service. The file does not exist yet, so let’s create it in config/file.config.js. Paste in the following:

module.exports = {  
  supportedMimes: {
    'text/csv': 'csv'
  },
  uploadsFolder: 'uploads',
  dbConnection: 'mongodb://127.0.0.1:27017/fileuploaddb'
}


The supportedMimes config will enable us to restrict which file types we will allow for uploads. The keys in the object are the mime types and the values are the file extensions.

The uploadsFolder configuration is used to specify the directory name for uploaded files. It is relative to the server root.

In the dbConnection configuration, we are specifying the connection string for our database. The Mongoose library will create the database if it does not exist.

Finally, let us create a method for querying the files. Paste in the following into our file.service.js file:

module.exports = {  
  getAll: (req, res, next) => {
    File.find((err, files) => {
      if (err) {
        return res.status(404).end();
      }
      console.log('File fetched successfully');
      res.send(files);
    });
  }
}


This exports an object with a method called getAll which fetches a list of files from the database. For now, the method only exists but isn’t connected to any route so the frontend has no way of accessing it yet. Let’s build our first route to fetch uploaded files.

Create a route file in routes/api.js. Add in the following:

const express = require('express');  
const router = express.Router();  
const fileService = require('../services/file.service.js');  
const app = express();

router.get('/files', fileService.getAll);  
module.exports = router;  


Before we start the server again, let’s paste the following into index.js:

app.use(bodyParser.json())  
const apiRoutes = require('./routes/api');  
app.use('/api', apiRoutes);  


Before visiting the route, we need one more step. With our current setup, MongoDB database needs to be running on port 27017. This port is the default port when the server is started without any arguments. To start the server with the default port run the command:

mongod  


To start it with a specific port, use the command:

mongod --port portnumber  


If you specify a port number, do not forget to update the port number in the config file, config/file.config.js in this line

dbConnection: 'mongodb://127.0.0.1:27017/fileuploaddb'  


Now, the route is ready to serve files from the backend. The registered route will live at the location localhost:3000/api/files. We do not have any files in the backend yet. If we visit the URL in the browser, we will get an empty array response. In the backend console, we should notice a message titled: File fetched successfully.

Do not forget to restart the Node.js server.

Build backend API for receiving files

At this stage, the backend application is able to connect to the database. Next, we will build the route API for receiving one or more files. First, we will only store the file locally. In the file services/file.service.js, alongside the getAll method, add the following:

uploadFile: (req, res, next) => {

}


This will receive the files but will not insert any information in the database yet. We will get to that in the next chapter.

In routes/api.js, add in the following line before the first route declaration:

const options = fileService.getFileOptions()  
const multer = require('multer')(options);

router.post('/upload', multer.any(), fileService.uploadFile);  


Here, we are including the multer library and providing some options for it. During the upload route declaration:

router.post('/upload', multer.any(), fileService.uploadFile);  


We are specifying the multer library as a middleware. This is so that it will intercept uploaded files and do some filtering for unaccepted files. The options for the library do not exist yet. Let’s add them in a method in the file services/file.service.js. Add in the method below:

getFileOptions: () => {  
  return {
    storage: multer.diskStorage({
      destination: fileConfig.uploadsFolder,
      filename: (req, file, cb) => {
        let extension = fileConfig.supportedMimes[file.mimetype]
        let originalname = file.originalname.split('.')[0]
        let fileName = originalname + '-' + (new Date()).getMilliseconds() + '.' + extension
        cb(null, fileName)
      }
    }),
    fileFilter: (req, file, cb) => {
      let extension = fileConfig.supportedMimes[file.mimetype]
      if (!extension) {
        return cb(null, false)
      } else {
        cb(null, true)
      }
    }
  }
}


This method returns some configuration for filename construction. It will set the destination for the uploaded file. Then it filters files so that we only upload the ones specified in the file config/file.config.js.

We cannot test the upload functionality with our current setup because we have not written the frontend yet. There is a tool called postman. It is designed exactly for that.

Store File meta-data in the database

Currently, the API route for receiving files only stores the file locally. Let’s modify the application so it also stores the file meta-data in the database. In the file services/file.service.js, modify the uploadFile method like below:

uploadFile: (req, res, next) => {  
  let savedModels = []
  async.each(req.files, (file, callback) => {
    let fileModel = new File({
      name: file.filename
    });
    fileModel.save((err) => {
      if (err) {
        return next('Error creating new file', err);
      }
      fileModel.encodedName = btoa(fileModel._id)
      fileModel.save((err) => {
        if (err) {
          return next('Error creating new file', err);
        }
        savedModels.push(fileModel)
        callback()
        console.log('File created successfully');
      })
    });
  }, (err) => {
    if (err) {
      return res.status(400).end();
    }
    return res.send(savedModels)
  })
}


After the multer library stores the files (locally) it will pass the file list to the callback above. The callback will create a unique hash for each file; then, it will store the file’s original name and hashed key in the database.

The async part is necessary because the meta-data insertion happens asynchronously for each file. We want to return a response to the frontend only when all the information has been saved.

Any file which fails the filter test of the multer middleware will not be passed to the uploadFile callback. If no files have been uploaded, we will return an empty array to the frontend. We can then deal with any validation however we wish.

Build Frontend for listing Files

Now let’s add functionality to our frontend so that it can list files from the backend. Navigate to the frontend folder. Start the development server using the command below:

npm run dev  


The frontend application will be running on the URL localhost:8080.

The first thing we need to do is allow the frontend to be able to send AJAX requests to the backend. To allow this during development, in the client root folder, let’s modify the file config/index.js. Modify the proxyTable key to:

proxyTable: {  
  '/api': 'http://localhost:3000',
    '/file': 'http://localhost:3000'
},


We have covered more details about the above configuration in a previous article, so check that if you’re facing any difficulties.

Let us create a component to list files. This will be responsible for fetching files from the backend. It will also loop over the list of returned files and create many instances of a child component called UploadedFile, which we will create later.

To begin with, create a component in src/components/UploadedFilesList.vue.

In there, paste in the following:

  
  
    # Files List


    
      
    
  




  
  


To list files, we are sending a request to the backend when the component is mounted. This is done using Axios.js, an HTTP library for Javascript. The component is initially created with an empty list of files. When the files list data is returned, we add it to the list of files.

Let’s create the UploadedFile component.Create a component file in src/components/UploadedFile.vue. In there, paste in the following:

  
  
    
      {{ file.name }}
      Delete
    
  




  


All that this component is currently doing is display the file name. The delete button does not currently perform any action but we will get to that later.

Next, let’s configure the router for our application so we can display the list of files.

In the src/router/index.js file, modify the router as shown below:

import Vue from 'vue'  
import Router from 'vue-router'  
import Main from '@/components/Main'

Vue.use(Router)  
export default new Router({  
  routes: [
    {
      path: '/',
      name: 'Main',
      component: Main
    }
  ]
})


The router is referencing the Main component file which does not exist yet. Let’s create it in src/components/Main.vue. Paste in the following:

  
  
    # Anonymous File Uploader System


    
      
    
  




  


Delete the existing file src/components/Hello.vue. It was created during the scaffolding stage and we will not need it. Bootup the frontend development server using npm run dev. If there aren’t any files in the backend, the list will be empty. If all goes well, we should see the text:

Files List  


Build Frontend for Uploading Files

At this stage, we can list files from the server. Our next task is to add the ability to upload files.

First, create a component for uploading files in src/components/UploadsContainer.vue. In there, paste the following:

  
  
    # Uploader

    
      <input
        v-show="!uploadStarted"
        type="file"
        multiple
        v-bind:name="uploadName"
        @change="fileSelected"
      >
      
Uploading...

    
    
      Start Upload      
      Cancel Upload
    
  




  


Add it to the dependencies of src/components/Main.vue as shown below. First, let us import it:

import UploadsContainer from '@/components/UploadsContainer'  


Then we list it as a child component:

components: {  
  UploadedFiles,
    UploadsContainer
},


Add a method as shown below:

methods: {  
  filesUploaded(files) {
    this.$refs.filesList.filesUploaded(files)
  }
}


Then, instantiate it in the template as shown below:

  
  
    # Anonymous File Uploader System

    
      
    
    
      
    
  
  


In the component src/components/UploadedFilesList.vue, add a method as below:

filesUploaded(files) {  
  files.forEach(file => {
    this.files.push(file)
  })
}


Let us break down what is happening in these components.

Inside src/components/UploadsContainer, we have a file upload input. Attached to it is a changed event handler called fileSelected:

@change="fileSelected"


When a file is selected, this handler is fired. The logic in this handler sets the selected files as a property in the component using the following:

let formData = new FormData()

for (let index = 0; index < files.length; index++) {  
  formData.append(name, files[index], files[index].name)
}

this.$set(this, 'formData', formData)  


This is using HTML5’s native FormData API.

Then we have a submit button:

Start Upload  


This calls a method named startUpload which is responsible for setting the status as actively uploading. Then, it calls another method which sends the formData property, containing the files to the backend.

If the upload was successful, we set the formData to null. Then, we emit an event to the parent container so it can update the uploaded files list using:

updateFilesList (files) {  
  this.$emit("files-uploaded", files)
}


If an error occurs, we show an alert to the user. We also have a cancel feature which will be triggered by the cancel button below:

Cancel Upload  


And it will only show when an upload process has started. The “start upload” button will only display when there is no upload in progress.

We are binding the form field to a property which specifies the key that will be used when sending files to the server:

v-bind:name="uploadName"  


The input field will also be hidden when an upload is in progress.

Onto the next file src/components/Main.vue. After instantiating UploadsContainer, we listen to an event using the syntax:

v-on:files-uploaded="filesUploaded"  


This will receive the uploaded files so we can pass it to a method named filesUploaded in the component src/components/UploadedFilesList.vue. This will make sure the list is updated.

Add support for file download

Frontend download setup

Now that we have the ability to upload files, let’s make sure we can download them.

First, create a component in src/components/FileDownloader.vue.

In there, paste the following:

  
  


  


This component includes an iframe in the template. Anytime the source for the iframe changes, it will make a request to that URL.

In the component src/components/UploadedFile.vue, include the downloader:

import FileDownloader from './FileDownloader'  


Let us register it first:

components: {  
  FileDownloader
},


Then we can use it in the template:

  


Add a method:

downloadFile(event) {  
  event.preventDefault()
  let url = event.target.href
  this.downloadKey += 1

  this.$nextTick().then(() => {
    this.$refs.downloader.downloadFile(url)
  })
}


Then, modify the link in the template as shown below:

{{ file.name }}  


This generates the appropriate URL by binding to the encodedName property of our file props.

Let’s make sure that the download is triggered on every click. We have to bind the download component’s key to a data property on the parent component.

Add a data property in src/components/UploadedFile.vue:

return {  
  downloadKey: 1
}


This key is incremented on each click of the download link. This forces the iframe to rerender and hence triggers the download.

Backend download setup

Now, the frontend is ready for making download requests. However, the backend has not been set up to serve the files yet. Let’s set it up now. In the backend file index.js, add the following lines before the call to start the server:

const fileRoute = require('./routes/file');  
app.use('/file', fileRoute);  


Next, create the route file routes/file.js. In there, add the content:

const express = require('express');  
const router = express.Router();  
const fileService = require('../services/file.service.js');  
router.get('/download/:name', fileService.downloadFile);  
module.exports = router;  


This sets up a route which accepts the hashed key of a file as an argument. This argument is then used to fetch the file from the database to get the real name of the file. Then, we reply with a download response.

Let’s set up the method handler for the route. Inside the file services/file.service.js, add a method to the exports as shown:

downloadFile(req, res, next) {  
  File.findOne({ name: req.params.name }, (err, file) => {
    if (err) {
      res.status(400).end();
    }

    if (!file) {
      File.findOne({ encodedName: req.params.name }, (err, file) => {
        if (err) {
          res.status(400).end();
        }
        if (!file) {
          res.status(404).end();
        }

        let fileLocation = path.join(__dirname, '..', 'uploads', file.name)

        res.download(fileLocation, (err) => {
          if (err) {
            res.status(400).end();
          }
        })
      })
    }
  })
}


When we restart the backend server, any file link on the frontend can now be clicked to download that file.

Add Frontend support for deleting files

Finally, let’s add functionality to delete files. Let’s work on the frontend first. In the frontend file src/components/UploadedFile.vue, add the method below:

deleteFile (file) {  
  this.$emit("delete-file", file);
},


Modify the delete button in the component to the following:

Delete  


Upon clicking the button, the component emits an event called delete-file to the parent.

Let’s modify the parent component src/components/UploadedFilesList.vue. Modify the UploadFile instantiation to the following:

<uploaded-file  
v-for="file in files"  
v-bind:file.sync="file"  
v-on:delete-file="deleteFile"  
>


In there, we add an event listener for the emitted child event we just made. This in turn calls a method named deleteFile in the parent. Let’s create that method:

deleteFile(file) {  
  if (confirm('Are you sure you want to delete the file?')) {
    axios.delete('/api/files/' + file._id)
      .then(() => {
        let fileIndex = this.files.indexOf(file)
        this.files.splice(fileIndex, 1)
      })
      .catch(() => {
        console.log("Error deleting file")
      })
  }


The frontend is ready to send AJAX requests to the backend.

Let’s set up the backend to receive the request. In the backend file routes/api.js, add the following line just before the export statement:

router.delete('/files/:id', fileService.deleteFile);  


Then, in the file services/file.service.js, add the method below:

deleteFile(req, res, next) {  
  File.findOne({ id: req.params._id }, (err, file) => {
    if (err) {
      res.status(400).end();
    }

    if (!file) {
      res.status(404).end();
    }

    let fileLocation = path.join(__dirname, '..', 'uploads', file.name)

    fs.unlink(fileLocation, () => {
      File.deleteOne(file, (err) => {
        if (err) {
          return next(err)
        }
        return res.send([])
      })
    })
  })
},


Now, we can delete files. When we click the delete link, we get an alert to confirm. If we click “ok”, the file is deleted from the backend folder and the information is removed from the database.

Conclusion

That brings us to the end of our article. We created a file upload service which is capable of many file uploads. It enables us to delete the files and we can download the file as well.

This is only a basic upload application. Possible expansions to this application could be advanced validation, upload progress, image preview feature, or multiple file downloads. Hopefully, this brought you some inspiration and ideas.

Vue.js Authentication System with Node.js Backend

Vue.js Authentication System with Node.js Backend

Vue.js Authentication System with Node.js Backend - In this tutorial, we'll explore different authentication types for JavaScript applications and build a Vue authentication system with a Node.js ...

Vue.js Authentication System with Node.js Backend - In this tutorial, we'll explore different authentication types for JavaScript applications and build a Vue authentication system with a Node.js ...

When building a Vue.js authentication system, there are two primary scenarios involved. Scenario one: one party controls both the front-end and back-end; scenario two: a third-party controls the back-end. If it is the latter case, we have to adapt the front-end. It has to deal with whatever authentication type becomes available from the back-end.

The finished code for this tutorial is available at these GitHub repositories:

Front-end JavaScript Authentication Types

In our application, we have the flexibility to choose between various authentication types. This is because we will be in charge of both front-end and back-end. Let us identify the two types. The first one is local or same-domain authentication — this is the case when the front-end and the back-end are both running on the same domain. The second is cross-domain authentication — it is when the front-end and back-end are running on different domains.

These are the two main categories but there are many sub-categories under them. In light of the above, we will use local authentication since we are in charge of the whole system. We will be using many Node.js libraries. But the two main ones are Passport.js and Express.js. Passport.js is an authentication library. It provides several features like local authentication, OAuth authentication and Single Sign-On authentication. Express.js is a server framework for Node.js used for building web applications.

The Application Paradigm

Our application front-end will have two main pages: a login page and a dashboard page. Both authenticated and anonymous users will have access to the login page. The dashboard page will only be accessible to authenticated users. The login page will have a form which will submit data through Ajax to our back-end API. Then, the back-end will check if the credentials are correct and reply back to the front-end with a cookie. This cookie is what the front-end will use to gain access to any locked pages.

Revalidation of the cookie happens on every request to a locked page. If the cookie becomes invalid or the user is not logged in, they cannot access the dashboard. The back-end will send an error response and the front-end will know to redirect the user back to the login page.

We will not be setting up a real database — we will use an array of users in the back-end to mimic some form of a database. Finally, we will have a logout link. This will send a request to our server to invalidate our current session and hence log out the current user.

So, let’s begin building our Vue.js authentication system using Node.js as a back-end.

Vue.js Front-End Setup

To begin with, we first need to have the latest version of Node.js and vue-cli setup. At the time of this article, the latest version of vue-cli is version 3. If the installed version is 2, we want to upgrade — we first need to remove the old version by running:

npm uninstall vue-cli -g


Then install the latest version by running:

npm install -g @vue/cli


followed by

npm install -g @vue/cli-init


After setting up the above, go to any folder in the terminal and run:

vue init webpack vueauthclient


This will create a new application in vueauthclient using the webpack folder organization.

We should get some prompts on the command line. It is safe to select all the defaults — but for this article, we can select “no” for the tests. Next, navigate to this folder using cd vueauthclient and run the application using:

npm run dev


This will launch a development server which is accessible at the URL localhost:8080. After visiting this URL, the Vue.js logo with some text should be visible on the page. The Vue.js component responsible for displaying this page lives in the file:

vueauthclient/src/components/HelloWorld.vue


Main Login Screen

Let us set up our login page. Then, we will change the homepage to default to the login page screen which we are yet to create. From now on, we will leave out the main application folder vueauthclient, when referring to files.

Let us install the Ajax library called Axios using:

npm install axios --save


This is a library which makes it easier to do HTTP Ajax calls to any back-end server. It is available for both front-end and back-end applications but here, we will only use it on the front-end.

Next, create a login component file in src/components/Login.vue. In this file, paste the following:

<template>
    <div>    
        <h2>Login</h2>    
        <form v-on:submit="login">    
            <input type="text" name="email" /><br>    
            <input type="password" name="password" /><br>    
            <input type="submit" value="Login" />    
        </form>    
    </div>
</template>

<script>
    import router from "../router"    
    import axios from "axios"    
    export default {    
        name: "Login",    
        methods: {    
            login: (e) => {    
                e.preventDefault()    
                let email = "[email protected]"   
                let password = "password"    
                let login = () => {    
                    let data = {    
                        email: email,    
                        password: password    
                    }    
                    axios.post("/api/login", data)    
                        .then((response) => {    
                            console.log("Logged in")    
                            router.push("/dashboard")    
                        })    
                        .catch((errors) => {    
                            console.log("Cannot log in")    
                        })    
                }    
                login()    
            }    
        }    
    }
</script>


Let’s break down this code to see what is happening.

The template part below is a form with two input fields: email and password. The form has a submit event handler attached to it. Using the Vue.js syntax v-on:submit="login", this will submit the field data to the login component method.

<template>
    <div>
        <h2>Login</h2>
        <form v-on:submit="login">
            <input type="text" name="email" /><br>
            <input type="password" name="password" /><br>    
            <input type="submit" value="Login" />    
        </form>    
    </div>
</template>


In the script part of the code, as shown below, we are importing our router file. This lives in src/router/index.js. We are also importing the Axios ajax library for the front-end. Then, we are storing the user credentials and making a login request to our back-end server:

<script>
    import router from "../router"        
    import axios from "axios"    
    export default {    
        name: "Login",    
        methods: {    
            login: (e) => {    
                e.preventDefault()   
                let email = "[email protected]"
                let password = "password"
                let login = () => {
                    let data = {
                        email: email,
                        password: password
                    }
                    axios.post("/api/login", data)
                        .then((response) => {
                            console.log("Logged in")
                            router.push("/dashboard")
                        })
                        .catch((errors) => {
                            console.log("Cannot login")
                        })
                }
                login()
            }
        }
    }
</script>


In the script area below,

e.preventDefault()
let email = "[[email protected]](mailto:[email protected])"
let password = "password"


We are storing hard-coded username and password in variables for now. This helps speed up development by preventing us from retyping the same thing. Later, we will switch those out and get the real data from the form submission.

In the final part of the code below, we are making an ajax call using the credentials above. In the case of an ok response from the server, we redirect the user to the dashboard. If the response is not ok, we stay on the same page and log an error in the console.

let login = () => {
  let data = {
    email: email,
    password: password
  }
  axios.post("/api/login", data)
    .then(response => {
      console.log("Logged in")
      router.push("/dashboard")
    })
    .catch(errors => {
      console.log("Cannot login")
    })
}
login()


Now that we have our login component set up, let’s change the router to make sure it recognizes the new page. In the file src/router/index.js, change the existing router to this:

import Vue from "vue"
import Router from "vue-router"
import Login from "@/components/Login"
import HelloWorld from "@/components/HelloWorld"
Vue.use(Router)
export default new Router({
  routes: [
    {
      path: "/",
      name: "HelloWorld",
      component: HelloWorld
    },
    {
      path: "/login",
      name: "Login",
      component: Login
    }
  ]
})


What we’ve done is import our new component, then add an object to the routes array. Remove the HelloWorld route registration, as we won’t be needing it anymore.

Finally, for the login, page, let’s make sure it is the default page of our application. Change the current path of the login route registration from

path: "/login",


to

path: "/",


Don’t forget to delete the route registration for the HelloWorld route or else an error might occur. Navigating to localhost:8080 again in the browser, we should see our new login form. Submitting it at this stage will not do anything except complain that the back-end URL localhost:8080/api/login does not exist.

Setup First Secure Page - The Dashboard

Now onto the dashboard page. Create a component for it by making a file at src/components/Dashboard.vue. In there, paste the following:

<template>
    <div>    
        <h2>Dashboard</h2>    
        <p>Name: {{ user.name }}</p>    
    </div>
</template>
<script>
    import axios from "axios"    
    import router from "../router"    
    export default {    
        name: "Login",    
        data() {    
            return {    
                user: {    
                    name: “Jesse”    
                }    
            }    
        },    
        methods: {    
            getUserData: function() {    
                let self = this    
                axios.get("/api/user")    
                    .then((response) => {    
                        console.log(response)    
                        self.$set(this, "user", response.data.user)    
                    })    
                    .catch((errors) => {    
                        console.log(errors)    
                        router.push("/")    
                    })    
            }    
        },    
        mounted() {    
            this.getUserData()    
        }    
    }
</script>


In the template section, we are displaying the current username. Before setting up the back-end, we will hardcode a user in the front-end. This is so that we can work with this page or else we will get an error.

In the script section, we are importing Axios library and our router. Then, we have a data function for our component where we return an object with a user property. As we can see, we currently have some hardcoded user data.

We also have two methods called getUserData and mounted. The Vue.js engine calls the mounted method when the component has opened. We only have to declare it. The second method, getUserData is called in the mounted method. In there, we are making a call to the back-end server to fetch the data for the currently logged in user.

During the call to the back-end, we get a response from the server. We will have to handle two possible scenarios depending on the response type.

First, if the call was successful, we set the user property with the data returned from the back-end using:

self.$set(this, "user", response.data.user)


Secondly, if there was a login issue, the server responds with an error. Then, the front-end redirects the user back to the login page with this line:

router.push("/")


We use the push method above for redirection and it is available in the package called vue-router, the default router for Vue.js. Let’s add in the route config for this page by adding this to the route file, like we did for the login page. Import the component:

import Dashboard from "@/components/Dashboard"


And add the route definition:

{
    path: "/dashboard",
    name: "Dashboard",
    component: Dashboard
}


Setup Front-End Data Layer with Axios

Now that we have our front-end pages in place, let’s configure Axios and Vue.js. We will make them ready to communicate with our back-end. Because we are in the development phase, the front-end is running on port 8080. Once we start developing our back-end server, it will be running on a different port number 3000. This will be the case until we are ready for production.

There is nothing stopping us from running them on the same port. In fact, will eventually be the case in the end. If we recollect, we are going for the same-domain approach. We will run the back-end and front-end on different ports for now. This is because we want to take advantage of the many useful features of the Vue.js development server. We will touch on how to merge the two (front and back-end) in a later chapter.

Before moving on, let’s highlight one issue here. There is a drawback to developing our application on different ports. It is called Cross-Origin Request Sharing, shortly named CORS. By default, it will not allow us to make cross-domain Ajax requests to our back-end. There is a Node.js library to find a way around that but we will leave that for another tutorial.

The Vue.js development server has something called proxying. It allows our back-end server to think that the front-end is running on the same port as itself. To enable that feature, open up the config file in config/index.js. Under the dev property, add in an object like so:

proxyTable: {

"/api": "http://localhost:3000"

},


In the above code, we are rerouting Ajax requests that begin with /api to the URL [http://localhost:3000](http://localhost:3000 "http://localhost:3000"). Notice that this is different from the URL our front-end application is running on. If we did not have this code, the Ajax requests by default are sent to [http://localhost:8080](http://localhost:8080 "http://localhost:8080"), which is not what we want. When ready for production, we can remove it:

Finally, install the front-end cookie library using:

npm install vue-cookies --save


Securing our Back-End API

Let’s now move onto setting up a Node.js back-end. First of all, we need to have Node.js installed on your system for this part as well. Head over to a terminal window. Create an empty folder called vueauthclient-backend. Navigate to the folder using:

cd vueauthclient-backend


Then initialize a new Node.js application using the command:

npm init


There will be several prompts. Let’s accept the defaults and specify values where required. We should end up with a file called package.json. Create a file called index.js in the project’s root directory. This is where our main code will live. Install several libraries using the command:

npm install --save body-parser cookie-session express passport passport-local


At the top of the index.js file, import the libraries using the code:

const express = require('express')

// creating an express instance
const app = express()
const cookieSession = require('cookie-session')
const bodyParser = require('body-parser')
const passport = require('passport')

// getting the local authentication type
const LocalStrategy = require('passport-local').Strategy


First, let’s initialize the cookie-session and the body-parser libraries using:

app.use(bodyParser.json())

app.use(cookieSession({
    name: 'mysession',
    keys: ['vueauthrandomkey'],
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
}))


We are setting the cookie to expire after 24 hours. Next, let’s instruct our Node.js app that we want to use Passport.js. Do that by adding the line:

app.use(passport.initialize());


Next, tell Passport.js to start its session management system:

app.use(passport.session());


Since we won’t be using a real database for managing users, for brevity’s sake we will use an array for that. Add in the following lines:

let users = [
  {
    id: 1,
    name: "Jude",
    email: "[email protected]",
    password: "password"
  },
  {
    id: 2,
    name: "Emma",
    email: "[email protected]",
    password: "password2"
  }
]


Next, let’s set up the URLs for logging in, logging out and getting user data. These will be found at POST /api/login, GET /api/logout and GET /api/user, respectively. For the login part, paste in the following:

app.post("/api/login", (req, res, next) => {
  passport.authenticate("local", (err, user, info) => {
    if (err) {
      return next(err);
    }

    if (!user) {
      return res.status(400).send([user, "Cannot log in", info]);
    }

    req.login(user, err => {
      res.send("Logged in");
    });
  })(req, res, next);
});


In here, we are instructing Express.js to authenticate the user using the supplied credentials. If an error occurs or if it fails, we return an error message to the front-end. If the user gets logged in, we will respond with a success message. Passport.js handles the checking of credentials. We will set that up shortly. Note that the method passport.authenticate resides in the Passport.js library.

The next URL we will set up is logout. This invalidates our cookie if one exists. Add this to achieve the functionality:

app.get("/api/logout", function(req, res) {
  req.logout();

  console.log("logged out")

  return res.send();
});


Finally, the URL to get the currently logged in users’ data. When logged in, Passport.js adds a user object to the request using the cookie from the front-end as an identifier. We have to use the id from that object to get the required user data from our array of data in the back-end. Paste in the following:

app.get("/api/user", authMiddleware, (req, res) => {
  let user = users.find(user => {
    return user.id === req.session.passport.user
  })

  console.log([user, req.session])

  res.send({ user: user })
})


Notice that, this time, we have a second variable we are passing in before the callback. This is because we want to protect this URL, so we are passing a middleware filter. This filter will check if the current session is valid before allowing the user to proceed with the rest of the operation. Let’s create the middleware using:

const authMiddleware = (req, res, next) => {
  if (!req.isAuthenticated()) {
    res.status(401).send('You are not authenticated')
  } else {
    return next()
  }
}


We have to make sure to declare it before creating the API route for /api/user.

Next, let’s configure Passport.js so it knows how to log us in. After logging in, it will store the user object data in a cookie-session, and retrieve the data on later requests. To configure Passport.js using the local strategy, add in the following:

passport.use(
  new LocalStrategy(
    {
      usernameField: "email",
      passwordField: "password"
    },

    (username, password, done) => {
      let user = users.find((user) => {
        return user.email === username && user.password === password
      })

      if (user) {
        done(null, user)
      } else {
        done(null, false, { message: 'Incorrect username or password'})
      }
    }
  )
)


In here, we are instructing Passport.js to use the LocalStrategy we created above. We are also specifying which fields to expect from the front-end as it needs a username and password. Then, we are using those values to query the user. If these are valid, we call the done callback, which will store the user object in the session. If it is not valid, we will call the done callback with a false value and return with an error. One thing to note is that the above code works in conjunction with the login URL. The call to passport.authenticate in that URL callback triggers the above code.

Next, let’s tell Passport.js how to handle a given user object. This is necessary if we want to do some work before storing it in session. In this case, we only want to store the id as it is enough to identify the user when we extract it from the cookie. Add in the following to achieve that:

passport.serializeUser((user, done) => {
  done(null, user.id)
})


Next, let’s set up the reverse. When a user makes a request for a secured URL. We tell passport how to retrieve the user object from our array of users. It will use the id we stored using the serializeUser method to achieve this. Add this:

passport.deserializeUser((id, done) => {
  let user = users.find((user) => {
    return user.id === id
  })

  done(null, user)
})


Now, let’s add the code which boots up the Node.js server using the following:

app.listen(3000, () => {
  console.log("Example app listening on port 3000")
})


Run the command:

node index.js


This actually starts the server. There will be a message in the console with the text Example app listening on port 3000.

Getting Ready for Production

Now, when we visit the page localhost:8080, we should see a login form. When we submit the form, we get redirected to the dashboard page. We achieve this using the proxy we set up earlier.

This is acceptable for development — but it defeats the purpose of having a same-domain application. To have a same-domain scenario, we need to compile our application for production.

Before that, let’s test that the proxy is working. Comment out the proxy URL code in config/index.js. We may need to restart the development server because we changed a config file.

Now, let’s revisit the login page and submit the form. We will get an error saying that we aren’t allowed access to the back-end server. To get around this, we need to configure our Node.js back-end server. The back-end will now serve our front-end application instead of the development server.

In the console for the front-end, run the command:

npm run build


This will generate all the necessary files needed for production. We can find all the created files from this command in the dist folder. From this point onwards, we have two options: we can either copy this folder over so it is part of our Node.js application or we can tell the Node.js server to refer directly to it on our file system. The latter is useful if we still want them as separate repositories. We will use the latter method.

Navigate to the folder dist. Run the command pwd to get the absolute path of the dist folder, assuming we are on a Linux based system or Mac. If we are on Windows, we can get the absolute path to the folder using an equivalent command.

Copy the absolute path but do not forget to restart the Node.js server after any modification. Since we do not want to keep restarting the server, let’s install nodemon. It can handle that for us when our code changes.

Next, paste in the following after the import statements:

const publicRoot = '/absolute/path/to/dist'

app.use(express.static(publicRoot))


This is telling the server where to look for files.

The final step will be to add a route to the root of our Node.js application. This is so it serves the production-ready code we had compiled. Do that by adding:

app.get("/", (req, res, next) => {
  res.sendFile("index.html", { root: publicRoot })
})


Now, even with the proxy disabled, let’s visit the server root localhost:3000. We will see the login form. Submit this and we should see the dashboard page with the username displayed.

Logout Functionality and Login Data

Note that our application is still using hardcoded data, we want to get that from the submitted form. Change these lines in the login component from:

let email = "[email protected]"

let password = "password"


to:

let email = e.target.elements.email.value

let password = e.target.elements.password.value


Now, we are using the data from the form. Next, let’s set up a link to log us out. In the component src/App.vue, change the template to this:

<template>
    <div id="app">    
        <img src="./assets/logo.png">    
        <div>    
            <router-link :to="{ name: 'Dashboard'}">Dashboard</router-link>    
            <router-link :to="{ name: 'Login'}">Login</router-link>    
            <a href="#" v-on:click="logout">Logout</a>    
        </div>    
        <router-view/>    
    </div>
</template>


Here, we have created links to the login page, the dashboard page, and a logout link. The logout link does not have a corresponding method currently, so let’s create that. In src/App.vue add a logout method in the scripts section:

logout: function (e) {
    axios
      .get("/api/logout")
      .then(() => {
        router.push("/")
      })
}


Here, we are making an Ajax request to the Node.js back-end. Then, we redirect the user to the login page when the response has returned. The logout will not work with our deployed app because we need to redeploy it again for production using:

npm run build


Now, we can revisit the URL localhost:3000. We can log in, log out and visit the dashboard page.

Conclusion

After this tutorial, we should be able to add as many authenticated pages as we want.

If this is the first time using Vue.js, please refer to our introductory blog post here. It will help in setting up and building a basic Vue.js application.

Also, don’t forget to protect you Vue.js application from code theft and reverse engineering. See our handy guide on protecting Vue.js apps with Jscrambler.

*Originally published by Jscrambler at *blog.jscrambler.com

=========================================================

Thanks for reading :heart: If you liked this post, share it with all of your programming buddies! Follow me on Facebook | Twitter

Learn More

☞ Nuxt.js - Vue.js on Steroids

☞ Vue JS 2 - The Complete Guide (incl. Vue Router & Vuex)

☞ Master Vuejs from scratch (incl Vuex, Vue Router)

☞ Vue JS 2.0 - Mastering Web Apps

☞ Vue.js Essentials - 3 Course Bundle

☞ MEVP Stack Vue JS 2 Course: MySQL + Express.js + Vue.js +PHP

Node.js, ExpressJs, MongoDB and Vue.js (MEVN Stack) Application Tutorial

Node.js, ExpressJs, MongoDB and Vue.js (MEVN Stack) Application Tutorial

In this tutorial, you'll learn how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB and how to build application with Node.js, ExpressJs, MongoDB and Vue.js

In this tutorial, you'll learn how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB and how to build application with Node.js, ExpressJs, MongoDB and Vue.js

Vue.js is a JavaScript framework with growing number of users. Released 4 years ago, it’s now one of the most populare front-end frameworks. There are some reasons why people like Vue.js. Using Vue.js is very simple if you are already familiar with HTML and JavaScript. They also provide clear documentation and examples, makes it easy for starters to learn the framework. Vue.js can be used for both simple and complex applications. If your application is quite complex, you can use Vuex for state management, which is officially supported. In addition, it’s also very flexible that yu can write template in HTML, JavaScript or JSX.

This tutorial shows you how to integrate Vue.js with Node.js backend (using Express framework) and MongoDB. As for example, we’re going to create a simple application for managing posts which includes list posts, create post, update post and delete post (basic CRUD functionality). I divide this tutorial into two parts. The first part is setting up the Node.js back-end and database. The other part is writing Vue.js code including how to build .vue code using Webpack.

Dependencies

There are some dependencies required for this project. Add the dependencies below to your package.json. Then run npm install to install these dependencies.

  "dependencies": {
    "body-parser": "~1.17.2",
    "dotenv": "~4.0.0",
    "express": "~4.16.3",
    "lodash": "~4.17.10",
    "mongoose": "~5.2.9",
    "morgan": "~1.9.0"
  },
  "devDependencies": {
    "axios": "~0.18.0",
    "babel-core": "~6.26.3",
    "babel-loader": "~7.1.5",
    "babel-preset-env": "~1.7.0",
    "babel-preset-stage-3": "~6.24.1",
    "bootstrap-vue": "~2.0.0-rc.11",
    "cross-env": "~5.2.0",
    "css-loader": "~1.0.0",
    "vue": "~2.5.17",
    "vue-loader": "~15.3.0",
    "vue-router": "~3.0.1",
    "vue-style-loader": "~4.1.2",
    "vue-template-compiler": "~2.5.17",
    "webpack": "~4.16.5",
    "webpack-cli": "^3.1.0"
  },

Project Structure

Below is the overview of directory structure for this project.

  app
    config
    controllers
    models
    queries
    routes
    views
  public
    dist
    src

The app directory contains all files related to server-side. The public directory contains two sub-directories: dist and src. dist is used for the output of build result, while src is for front-end code files.

Model

First, we define a model for Post using Mongoose. To make it simple, it only has two properties: title and content.

app/models/Post.js

  const mongoose = require('mongoose');

  const { Schema } = mongoose;

  const PostSchema = new Schema(
    {
      title: { type: String, trim: true, index: true, default: '' },
      content: { type: String },
    },
    {
      collection: 'posts',
      timestamps: true,
    },
  );

  module.exports = mongoose.model('Post', PostSchema);

Queries

After defining the model, we write some queries that will be needed in the controllers.

app/queries/posts.js

  const Post = require('../models/Post');

  /**
   * Save a post.
   *
   * @param {Object} post - Javascript object or Mongoose object
   * @returns {Promise.}
   */
  exports.save = (post) => {
    if (!(post instanceof Post)) {
      post = new Post(post);
    }

    return post.save();
  };

  /**
   * Get post list.
   * @param {object} [criteria] - Filter options
   * @returns {Promise.<Array.>}
   */
  exports.getPostList = (criteria = {}) => Post.find(criteria);

  /**
   * Get post by ID.
   * @param {string} id - Post ID
   * @returns {Promise.}
   */
  exports.getPostById = id => Post.findOne({ _id: id });

  /**
   * Delete a post.
   * @param {string} id - Post ID
   * @returns {Promise}
   */
  exports.deletePost = id => Post.findByIdAndRemove(id);

Controllers

We need API controllers for handling create post, get post listing, get detail of a post, update a post and delete a post.

app/controllers/api/posts/create.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.save(req.body)
    .then((post) => {
      if (!post) {
        return Promise.reject(new Error('Post not created'));
      }

      return res.status(200).send(post);
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to create post');
    });

app/controllers/api/posts/delete.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.deletePost(req.params.id)
    .then(() => res.status(200).send())
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to delete post');
    });

app/controllers/api/posts/details.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostById(req.params.id)
    .then((post) => {
      if (!post) {
        return Promise.reject(new Error('Post not found'));
      }

      return res.status(200).send(post);
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to get post');
    });

app/controllers/api/posts/list.js

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostList(req.params.id)
    .then(posts => res.status(200).send(posts))
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to get post list');
    });

app/controllers/api/posts/update.js

  const _ = require('lodash');

  const postQueries = require('../../../queries/posts');

  module.exports = (req, res) => postQueries.getPostById(req.params.id)
    .then(async (post) => {
      if (!post) {
        return Promise.reject(new Error('Post not found'));
      }

      const { title, content } = req.body;

      _.assign(post, {
        title, content
      });

      await postQueries.save(post);

      return res.status(200).send({
        success: true,
        data: post,
      })
    })
    .catch((err) => {
      console.error(err);

      return res.status(500).send('Unable to update post');
    });

Routes

We need to have some pages for user interaction and some API endpoints for processing HTTP requests. To make the app scalable, it’s better to separate the routes for pages and APIs.

app/routes/index.js

  const express = require('express');

  const routes = express.Router();

  routes.use('/api', require('./api'));
  routes.use('/', require('./pages'));

  module.exports = routes;


Below is the API routes.

app/routes/api/index.js

  const express = require('express');

  const router = express.Router();

  router.get('/posts/', require('../../controllers/api/posts/list'));
  router.get('/posts/:id', require('../../controllers/api/posts/details'));
  router.post('/posts/', require('../../controllers/api/posts/create'));
  router.patch('/posts/:id', require('../../controllers/api/posts/update'));
  router.delete('/posts/:id', require('../../controllers/api/posts/delete'));

  module.exports = router;


For the pages, in this tutorial, we use plain HTML file. You can easily replace it with any HTML template engine if you want. The HTML file contains a div whose id is app. Later, in Vue.js application, it will use the element with id app for rendering the content. What will be rendered on each pages is configured on Vue.js route on part 2 of this tutorial.

app/routes/pages/index.js

  const express = require('express');

  const router = express.Router();

  router.get('/posts/', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  router.get('/posts/create', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  router.get('/posts/:id', (req, res) => {
    res.sendFile(`${__basedir}/views/index.html`);
  });

  module.exports = router;

Below is the HTML file

app/views/index.html

  <!DOCTYPE html>
  <html>
    <head>
      <meta charset="utf-8">
      <title>VueJS Tutorial by Woolha.com</title>
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.4.0/css/font-awesome.min.css" type="text/css" media="all" />
      <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
      <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
    </head>
    <body>
      <div id="app"></div>
      <script src="/dist/js/main.js"></script>
    </body>
  </html>

Below is the main script of the application, you need to run this for starting the server-side application.

app/index.js

  require('dotenv').config();

  const bodyParser = require('body-parser');
  const express = require('express');
  const http = require('http');
  const mongoose = require('mongoose');
  const morgan = require('morgan');
  const path = require('path');

  const dbConfig = require('./config/database');
  const routes = require('./routes');

  const app = express();
  const port = process.env.PORT || 4000;

  global.__basedir = __dirname;

  mongoose.Promise = global.Promise;

  mongoose.connect(dbConfig.url, dbConfig.options, (err) => {
    if (err) {
      console.error(err.stack || err);
    }
  });

  /* General setup */
  app.use(morgan('dev'));
  app.use(bodyParser.json());
  app.use(bodyParser.urlencoded({ extended: true }));
  app.use(morgan('dev'));

  app.use('/', routes);

  const MAX_AGE = 86400000;

  // Select which directories or files under public can be served to users
  app.use('/', express.static(path.join(__dirname, '../public'), { maxAge: MAX_AGE }));

  // Error handler
  app.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
    res.status(err.status || 500);

    if (err.status === 404) {
      res.locals.page = {
        title: 'Not Found',
        noIndex: true,
      };

      console.error(`Not found: ${req.url}`);

      return res.status(404).send();
    }

    console.error(err.stack || err);

    return res.status(500).send();
  });

  http
    .createServer(app)
    .listen(port, () => {
      console.info(`HTTP server started on port ${port}`);
    })
    .on('error', (err) => {
      console.error(err.stack || err);
    });

  process.on('uncaughtException', (err) => {
    if (err.name === 'MongoError') {
      mongoose.connection.emit('error', err);
    } else {
      console.error(err.stack || err);
    }
  });

  module.exports = app;

That’s all for the server side preparation. On the next part, we’re going to set up the Vue.js client-side application and build the code into a single JavaScript file ready to be loaded from HTML.

Then, we build the code using Webpack, so that it can be loaded from HTML. In this tutorial, we’re building a simple application with basic CRUD functionality for managing posts.

Create Vue.js Components

For managing posts, there are three components we’re going to create. The first one is for creating a new post. The second is for editing a post. The other is for managing posts (displaying list of posts and allow post deletion)

First, this is the component for creating a new post. It has one method createPost which validate data and send HTTP request to the server. We use axios for sending HTTP request.

public/src/components/Posts/Create.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Create a Post</h1>
      <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
          <li v-for="error in errors">{{ error }}</li>
        </ul>
      </p>
      <b-form @submit.prevent>
        <b-form-group>
          <b-form-input type="text" class="form-control" placeholder="Title of the post" v-model="post.title"></b-form-input>
        </b-form-group>
        <b-form-group>
          <b-form-textarea class="form-control" placeholder="Write the content here" v-model="post.content"></b-form-textarea>
        </b-form-group>
        <b-button variant="primary" v-on:click="createPost">Create Post</b-button>
      </b-form>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        errors: [],
        post: {
          title: '',
          content: '',
        },
      }),
      methods: {
        createPost(event) {
          if (event) {
            event.preventDefault();
          }

          if (!this.post.title) {
            this.errors = [];

            if (!this.post.title) {
              this.errors.push('Title required.');
            }

            return;
          }

          const url = 'http://localhost:4000/api/posts';
          const param = this.post;

          axios
            .post(url, param)
            .then((response) => {
              console.log(response);
              window.location.href = 'http://localhost:4000/posts';
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


Below is the component for editing a post. Of course, we need the current data of the post before editing it. Therefore, there’s fetchPost method called when the component is created. There’s also updatePost method which validate data and call the API for updating post.

public/src/components/Posts/Edit.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Edit a Post</h1>
      <p v-if="errors.length">
        <b>Please correct the following error(s):</b>
        <ul>
          <li v-for="error in errors">{{ error }}</li>
        </ul>
      </p>
      <b-form @submit.prevent>
        <b-form-group>
          <b-form-input type="text" class="form-control" placeholder="Title of the post" v-model="post.title"></b-form-input>
        </b-form-group>
        <b-form-group>
          <b-form-textarea class="form-control" placeholder="Write the content here" v-model="post.content"></b-form-textarea>
        </b-form-group>
        <b-button variant="primary" v-on:click="updatePost">Update Post</b-button>
      </b-form>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        errors: [],
        post: {
          _id: '',
          title: '',
          content: '',
        },
      }),
      created: function() {
        this.fetchPost();
      },
      methods: {
        fetchPost() {
          const postId = this.$route.params.id;
          const url = `http://localhost:4000/api/posts/${postId}`;

          axios
            .get(url)
            .then((response) => {
              this.post = response.data;
              console.log('this.post;');
              console.log(this.post);
          });
        },
        updatePost(event) {
          if (event) {
            event.preventDefault();
          }

          if (!this.post.title) {
            this.errors = [];

            if (!this.post.title) {
              this.errors.push('Title required.');
            }

            return;
          }

          const url = `http://localhost:4000/api/posts/${this.post._id}`;
          const param = this.post;

          axios
            .patch(url, param)
            .then((response) => {
                console.log(response);
              window.alert('Post successfully saved');
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


For managing posts, we need to fetch the list of post first. Similar to the edit component, in this component, we have fetchPosts method called when the component is created. For deleting a post, there’s also a method deletePost. If post successfully deleted, the fetchPosts method is called again to refresh the post list.

public/src/components/Posts/List.vue

  <template>
    <b-container>
      <h1 class="d-flex justify-content-center">Post List</h1>
      <b-button variant="primary" style="color: #ffffff; margin: 20px;"><a href="/posts/create" style="color: #ffffff;">Create New Post</a></b-button>
      <b-container-fluid v-if="posts.length">
        <table class="table">
          <thead>
            <tr class="d-flex">
              <td class="col-8">Titleqqqqqqqqq</td>
              <td class="col-4">Actions</td>
            </tr>
          </thead>
          <tbody>
            <tr v-for="post in posts" class="d-flex">
              <td class="col-8">{{ post.title }}</td>
              <td class="col-2"><a v-bind:href="'http://localhost:4000/posts/' + post._id"><button type="button" class="btn btn-primary"><i class="fa fa-edit" aria-hidden="true"></i></button></a></td>
              <td class="col-2"><button type="button" class="btn btn-danger" v-on:click="deletePost(post._id)"><i class="fa fa-remove" aria-hidden="true"></i></button></td>
            </tr>
          </tbody>
        </table>
      </b-container-fluid>
    </b-container>
  </template>

  <script>
    import axios from 'axios';

    export default {
      data: () => ({
        posts: [],
      }),
      created: () => {
        this.fetchPosts();
      },
      methods: {
        fetchPosts() {
          const url = 'http://localhost:4000/api/posts/';

          axios
            .get(url)
            .then((response) => {
              console.log(response.data);
              this.posts = response.data;
          });
        },
        deletePost(id) {
          if (event) {
            event.preventDefault();
          }

          const url = `http://localhost:4000/api/posts/${id}`;
          const param = this.post;

          axios
            .delete(url, param)
            .then((response) => {
              console.log(response);
              console.log('Post successfully deleted');

              this.fetchPosts();
            }).catch((error) => {
              console.log(error);
            });
        },
      }
    }
  </script>


All of the components above are wrapped into a root component which roles as the basic template. The root component renders the navbar which is same across all components. The component for each routes will be rendered on router-view.

public/src/App.vue

  <template>
    <div>
      <b-navbar toggleable="md" type="dark" variant="dark">
        <b-navbar-toggle target="nav_collapse"></b-navbar-toggle>
        <b-navbar-brand to="/">My Vue App</b-navbar-brand>
        <b-collapse is-nav id="nav_collapse">
          <b-navbar-nav>
            <b-nav-item to="/">Home</b-nav-item>
            <b-nav-item to="/posts">Manage Posts</b-nav-item>
          </b-navbar-nav>
        </b-collapse>
      </b-navbar>
      <!-- routes will be rendered here -->
      <router-view />
    </div>
  </template>

  <script>

  export default {
    name: 'app',
    data () {},
    methods: {}
  }
  </script>


For determining which component should be rendered, we use Vue.js’ router. For each routes, we need to define the path, component name and the component itself. A component will be rendered if the current URL matches the path.

public/src/router/index.js

  import Vue from 'vue'
  import Router from 'vue-router'

  import CreatePost from '../components/Posts/Create.vue';
  import EditPost from '../components/Posts/Edit.vue';
  import ListPost from '../components/Posts/List.vue';

  Vue.use(Router);

  let router = new Router({
    mode: 'history',
    routes: [
      {
        path: '/posts',
        name: 'ListPost',
        component: ListPost,
      },
      {
        path: '/posts/create',
        name: 'CreatePost',
        component: CreatePost,
      },
      {
        path: '/posts/:id',
        name: 'EditPost',
        component: EditPost,
      },
    ]
  });

  export default router;


Lastly, we need a main script as the entry point which imports the main App component and the router. Inside, it creates a new Vue instance

webpack.config.js

  import BootstrapVue from 'bootstrap-vue';
  import Vue from 'vue';

  import App from './App.vue';
  import router from './router';

  Vue.use(BootstrapVue);
  Vue.config.productionTip = false;
  new Vue({
    el: '#app',
    router,
    render: h => h(App),
  });

Configure Webpack

For building the code into a single JavaSript file. Below is the basic configuration for Webpack 4.

webpack.config.js

  const { VueLoaderPlugin } = require('vue-loader');

  module.exports = {
    entry: './public/src/main.js',
    output: {
      path: `${__dirname}/public/dist/js/`,
      filename: '[name].js',
    },
    resolve: {
      modules: [
        'node_modules',
      ],
      alias: {
        // vue: './vue.js'
      }
    },
    module: {
      rules: [
        {
          test: /\.css$/,
          use: [
            'vue-style-loader',
            'css-loader'
          ]
        },
        {
          test: /\.vue$/,
          loader: 'vue-loader',
          options: {
            loaders: {
            }
            // other vue-loader options go here
          }
        },
        {
          test: /\.js$/,
          loader: 'babel-loader',
          exclude: /node_modules/
        },
      ]
    },
    plugins: [
      new VueLoaderPlugin(),
    ]

After that, run ./node_modules/webpack/bin/webpack.js. You can add the command to the scripts section of package.json, so you can run Webpack with a shorter command npm run build, as examplified below.

  "dependencies": {
    ...
  },
  "devDependencies": {
    ...
  },
  "scripts": {
    "build": "./node_modules/webpack/bin/webpack.js",
    "start": "node app/index.js"
  },

Finally, you can start to try the application. This code is also available on Woolha.com’s Github.

How to implement server-side pagination in Vue.js with Node.js

How to implement server-side pagination in Vue.js with Node.js

This is a simple example of how to implement server-side pagination in Vue.js with a Node.js backend API.

Originally published at https://jasonwatmore.com
This is a simple example of how to implement pagination in Vue.js with a Node.js backend API.

The example contains a hard coded array of 150 objects split into 30 pages (5 items per page) to demonstrate how the pagination logic works. Styling of the example is done with Bootstap 4.

The tutorial code is available on GitHub at https://github.com/cornflourblue/vue-node-server-side-pagination.

Here it is in action (may take a few seconds for the container to startup):

(See on CodeSandbox at https://codesandbox.io/s/vuejs-node-server-side-pagination-example-0l40x)

Running the Vue.js + Node Pagination Example Locally
  1. Install NodeJS and NPM from https://nodejs.org.
  2. Download or clone the tutorial project source code from https://github.com/cornflourblue/vue-node-server-side-pagination.
  3. Install required npm packages of the backend Node API by running the npm install command in the /server folder.
  4. Start the backend Node API by running npm start in the /server folder,
  5. this will start the API on the URL http://localhost:4000.
  6. Install required npm packages of the frontend Vue.js app by running the npm install command in the /client folder.
  7. Start the Vue.js frontend app by running npm start in the /client folder,
  8. this will build the app with webpack and automatically launch it in a
  9. browser on the URL http://localhost:8080.
Server-Side (Node.js) Pagination Logic

Pagination is handled by the backend Node API with the help of the jw-paginate npm package, for more info on how the pagination logic works see JavaScript - Pure Pagination Logic in Vanilla JS / TypeScript.

Below is the code for the paged items route (/api/items) in the node server file (/server/server.js) in the example, it creates a hardcoded list of 150 items to be paged, in a real application you would replace this with real data (e.g. from a database). The route accepts an optional page parameter in the url query string, if the parameter isn't set it defaults to the first page.

The paginate() function is from the jw-paginate package and accepts the following parameters:

  • totalItems (required) - the total number of items to be paged
  • currentPage (optional) - the current active page, defaults to the first page
  • pageSize (optional) - the number of items per page, defaults to 10
  • maxPages (optional) - the maximum number of page navigation links to display, defaults to 10

The output of the paginate function is a pager object containing all the information needed to get the current pageOfItems out of the items array, and to display the pagination controls in the Vue.js frontend, including:

  • startIndex - the index of the first item of the current page (e.g. 0)
  • endIndex - the index of the last item of the current page (e.g. 9)
  • pages - the array of page numbers to display (e.g. [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ])
  • currentPage - the current active page (e.g. 1)
  • totalPages - the total number of pages (e.g. 30)

I've set the pageSize to 5 in the CodeSandbox example above so the pagination links aren't hidden below the terminal console when the container starts up. In the code on GitHub I didn't set the page size so the default 10 items are displayed per page in that version.

The current pageOfItems is extracted from the items array using the startIndex and endIndex from the pager object. The route then returns the pager object and current page of items in a JSON response.

// paged items route
app.get('/api/items', (req, res, next) => {
   // example array of 150 items to be paged
   const items = [...Array(150).keys()].map(i => ({ id: (i + 1), name: 'Item ' + (i + 1) }));

   // get page from query params or default to first page
   const page = parseInt(req.query.page) || 1;

   // get pager object for specified page
   const pageSize = 5;
   const pager = paginate(items.length, page, pageSize);

   // get page of items from items array
   const pageOfItems = items.slice(pager.startIndex, pager.endIndex + 1);

   // return pager object and current page of items
   return res.json({ pager, pageOfItems });
});

Client-Side (Vue.js) Pagination Component

Since the pagination logic is handled on the server, the only thing the Vue.js client needs to do is fetch the pager information and current page of items from the backend, and display them to the user.

Vue.js Home Page Component

Below is the Vue home page component (/client/src/home/HomePage.vue) from the example. The template renders the current page of items as a list of divs with the v-for directive, and renders the pagination controls using the data from the pager object. Each pagination link sets the page query parameter in the url with the <router-link> component and :to="{ query: { page: ... }}" property.

The Vue component contains a watcher function on the page url query parameter '$route.query.page', the handler function is triggered by Vue whenever the page variable in the url querystring changes, the immediate: true flag tells Vue to also run the function when the component first loads. The watcher function checks if the page has changed and fetches the pager object and pageOfItems for the current page from the backend API with an HTTP request.

The CSS classes used are all part of Bootstrap 4.3, for more info see https://getbootstrap.com/docs/4.3/getting-started/introduction/.

<template>
   <div class="card text-center m-3">
       <h3 class="card-header">Vue.js + Node - Server Side Pagination Example</h3>
       <div class="card-body">
           <div v-for="item in pageOfItems" :key="item.id">{{item.name}}</div>
       </div>
       <div class="card-footer pb-0 pt-3">
           <ul v-if="pager.pages && pager.pages.length" class="pagination">
               <li :class="{'disabled':pager.currentPage === 1}" class="page-item first-item">
                   <router-link :to="{ query: { page: 1 }}" class="page-link">First</router-link>
               </li>
               <li :class="{'disabled':pager.currentPage === 1}" class="page-item previous-item">
                   <router-link :to="{ query: { page: pager.currentPage - 1 }}" class="page-link">Previous</router-link>
               </li>
               <li v-for="page in pager.pages" :key="page" :class="{'active':pager.currentPage === page}" class="page-item number-item">
                   <router-link :to="{ query: { page: page }}" class="page-link">{{page}}</router-link>
               </li>
               <li :class="{'disabled':pager.currentPage === pager.totalPages}" class="page-item next-item">
                   <router-link :to="{ query: { page: pager.currentPage + 1 }}" class="page-link">Next</router-link>
               </li>
               <li :class="{'disabled':pager.currentPage === pager.totalPages}" class="page-item last-item">
                   <router-link :to="{ query: { page: pager.totalPages }}" class="page-link">Last</router-link>
               </li>
           </ul>
       </div>
   </div>
</template>

<script>
export default {
   data () {
       return {
           pager: {},
           pageOfItems: []
       }
   },
   watch: {
       '$route.query.page': {
           immediate: true,
           handler(page) {
               page = parseInt(page) || 1;
               if (page !== this.pager.currentPage) {
                   fetch(`/api/items?page=${page}`, { method: 'GET' })
                       .then(response => response.json())
                       .then(({pager, pageOfItems}) => {
                           this.pager = pager;
                           this.pageOfItems = pageOfItems;
                       });
               }
           }
       }
   }
}
</script>