How to build a simple social media monitor with NodeJS, GraphQL, and Vue

We’ll build a simple monitor that will track the number of people that followed us on specific dates, using Medium as the use case. This is just a bare prototype and can be done for any other social media or networking platform.

Introduction

We’ll build a simple monitor that will track the number of people that followed us on specific dates, using Medium as the use case. This is just a bare prototype and can be done for any other social media or networking platform.


In the end, we should have something like this:

To achieve this, we will use the node-imap package. The two major protocols for handling emails are IMAP (Internet Messaged Access Protocol) and POP (Post office protocol). IMAP is preferred, because it always syncs with the mail server, hence the changes made on the mail client will appear immediately on the webmail inbox.

Prerequisites

  • NodeJS
  • VueJS
  • A Gmail account

Setup the back-end with node-imap and apollo

Firstly, install the necessary packages.


npm i --save node-imap apollo-server mailparser

Now you can define the types and resolver and then run the apollo server.

const {ApolloServer, gql} = require('apollo-server')
	const typeDefs = gql`
	    type User {
	        email: String!
	        password: String!
		}
	    type Mutation {
	        imapMutation(email: String!, password: String!): User!
	    }
	`
	const resolvers = {
		Mutation:{
			imapMutation: (parent, args) => {}
		}
	}
	const server = new ApolloServer({typeDefs, resolvers})
	server.listen({port: process.env.PORT || 4000}).then(({url})=>{
		console.log(`Server started at ${url}, \n Unai Way ✈️ ✈️ ✈️` )
	})

A ‘User’ type that has the email and password string types as its schema has been defined, and a mutation called ‘imapMutation’ which receives the email and password from the user and then returns a response with the User type.

The resolver handles the mutation, and then you can work with the arguments sent from the client.

Now you can run the server.

You can then import the node-imap and mailparser modules. The mailparser module will be used to receive the mail responses as JSON.

const Imap = require('imap')
const simpleParser = require('mailparser').simpleParser

You are going to create a ‘connectImap’ function that will handle our IMAP functionality. From the node-imap documentation, you can get the skeleton of how the module works, and then copy and paste it into the code. It basically works with callbacks and emitters, so we’ll wrap it in a promise.

You should have something like this.

//Create an array to hold the graphPoints
	let graphPoints = [];
async function connectToImap(user,password) {
	let resolveGraph = new Promise((resolve,reject)=>{
		const imap = new Imap({
			user: user,
			password: password,
			host: 'imap.gmail.com',
			port: 993,
			tls: true
		})
		function openInbox(cb) {
			imap.openBox('INBOX', true, cb)
		}
		imap.once('ready', function() {
			openInbox(function(err, box) {
				if (err) throw new Error('Invalid Login. Please Try again.')
					f.on('message', function(msg, seqno) {
					   msg.on('body', function(stream, info) {
						let buffer = ''
						stream.on('data', function(chunk) {
						  buffer += chunk.toString('utf8')
						})
					   })	
					})
					f.once('error', function(err) {
						reject(new Error('Fetching results failed'))
					})
					f.once('end', function() {
						console.log('Done fetching all messages!')
						imap.end()
					})
				})
			})
		})
		imap.once('error', function(err) {
			reject(new Error('Failed Imap connection'))
		})
		imap.once('end', function() {
			console.log('Connection ended')
		})
		imap.connect()
	})
	return await resolveGraph
}

When the ‘ready’ event is called, we connect to our mail, and then we can search for messages. So, we’ll search for emails from the medium account that handles followers ([email protected]).

Our ‘ready’ event should look like this.

imap.once('ready', function() {
//Open Inbox
openInbox(function(err, box) {
if (err) throw new Error('Invalid Login. Please Try again.')
//Search for mails sent from the medium account
imap.search([['FROM',
'[email protected]',
]], (err,result) =>{
//Get the headers from the mail
var f = imap.fetch(result, {
bodies: ['HEADER.FIELDS (FROM SUBJECT DATE)'],
struct: true
})
f.on('message', function(msg, seqno) {
msg.on('body', function(stream, info) {
let buffer = ''
stream.on('data', function(chunk) {
buffer += chunk.toString('utf8')
simpleParser(buffer).then(parsed => {
//Check if the subject of each mail had a 'started following you' substring
if(parsed.subject && parsed.subject.includes('started following you')){
//Split the array to get the total number of followers, and remove empty spaces
let numberOfFollowers = parsed.subject.split(/,|and/).filter(v=>v!=' ').length
//Handle the instance where the mail says 'paschal and 3 others followed you'
if(/\d others/.test(parsed.subject)) {
let otherNumber = /\d/.exec(parsed.subject)
numberOfFollowers += (Number(otherNumber[0]) - 1)
}
//Create an object with the number of followers and the date
let dataPoint = {
numberOfFollowers: numberOfFollowers,
date: parsed.date
}
//Push to graphPoints array
graphPoints.push(dataPoint)
}
})
.catch(err =>{throw err})
})
})
})
f.once('error', function(err) {
reject(new Error('Fetching results failed'))
})
f.once('end', function() {
console.log('Done fetching all messages!')
imap.end()
})
})
})
})
imap.once('error', function(err) {
reject(new Error('Failed Imap connection'))
})

imap.once('end', function() {
  console.log('Connection ended')
  resolve(graphPoints)
})
imap.connect()

We search for emails where each subject contains a ‘started following you’ substring. Then, we split the arrays with either a comma or the conjunction ‘and’ to get the number of followers, and then handle cases like ‘Peter and 3 others started following you’. After splitting the string above, we will have an output:

[
'Peter',
'3 others started following you'
]

The length of this array is 2, so we have two followers. If the subject contains ‘others’, we take the digit behind it and add to the length of the array, which is 5 and then we subtract 1 to get rid of the ‘3 others started following you’ string. This leaves us with 4.

Then, we resolve the promise when the ‘end’ event is fired (imap.once(‘end’)).

Since we will have to send the array to our apollo client, we will need to define the type of the ‘graphPoints’ array.

Our type definitions should look like this:

const typeDefs = gql`

	scalar Date

    # Make a type user
    type User {
        email: String!
        password: String!
        data: [Graph!]
	}
	
	type Graph {
		numberOfFollowers: Int!
		date: Date!
	}

    type Mutation {
        imapMutation(email: String!, password: String!): User!
    }
`

We added the data key to the ‘User’ type, which will hold the ‘graphPoints’ value, and its type is an array of objects with the ‘Graph’ type.

Finally, we handle the resolver, which will get the email and the password of the user and then return the email and the data (graphPoints).

const resolvers = {
Mutation:{
imapMutation: (parent, args) => {
//Empty the array before each response
graphPoints = []
/*call the connectToImap function with the email and password
sent by the user */
return connectToImap(args.email,args.password)
.then(response =>{
const user = {
email: args.email,
data: response
}
//Send the response
return user
})
.catch(error =>{
return new Error('Invalid Credentials. Please Try again.')
})
}
}
}

If we log the user object, our structure should be something like this:

email: String,
data: [ { numberOfFollowers: 1, date: 2017-07-05T07:53:18.000Z },
{ numberOfFollowers: 1, date: 2017-07-07T19:34:57.000Z }
]

Setup the front-end with v-charts and apollo client

Now, we want to get the data sent from the server and plot the chart with the v-charts module.


But first, we install our dependencies.

npm install --save vue-chartjs vue-apollo apollo-client apollo-link-http apollo-cache-persist apollo-cache-inmemory graphql graphql-tag moment

I know what you’re thinking — that’s a lot of dependencies. If you have troubles setting up a Vue project, you can find out how to do that here. We should also include vuetify and the vue-router if we want to style the project and create additional routes.

In our ‘src’ folder, we can create a ‘config’ folder. The structure should look like this:

|src
|config
-graphql.js
-index.js
-LineChart.js
|pages
-login.vue
|router
-index.js
App.vue
main.js

We will have to set up our graphql client in the src/config/index.js file.

import {ApolloClient} from 'apollo-client'
import {HttpLink} from 'apollo-link-http'
import {InMemoryCache} from 'apollo-cache-inmemory'
import {CachePersistor} from 'apollo-cache-persist'

const httpLink = new HttpLink({
    uri:"http://localhost:4000"
})
export const cache = new InMemoryCache()


const apolloClient = new ApolloClient({
    link: httpLink,
    cache: cache
})


export const persistor = new CachePersistor({
    cache,
    storage: window.localStorage
})


export default apolloClient

Make sure the uri is on the same port as your apollo server, by default the apollo server runs on port 4000. Our apollo client is then set up with the httpLink and the cache.

The src/config/graphql.js file should look like this:

import gql from 'graphql-tag'

export const IMAP_MUTATION = gql `mutation imapMutation($email: String!, $password: String!) {
    imapMutation(
        email: $email,
        password: $password
    ){
        email
        data{
            numberOfFollowers
            date
        }
    }
}`

This query will submit the email and the password of the user to the imapMutation and then get the email and the data(graphPoints) from the apollo server.

Then, we create our chart component in the src/config/LineChart.js file. We can use charts ranging from bar charts to histograms. A line chart was used in this example.

import {Line, mixins} from 'vue-chartjs'
const {reactiveProp} = mixins

export default {
    extends: Line,
    mixins: [reactiveProp],
    props:['options'],
    mounted(){
        this.renderChart(this.chartData,this.options)
    }
}

We can import the ‘vue-apollo’ package in our main.js file and include the apolloProvider when initializing the app.

import Vue from 'vue'
import './plugins/vuetify'
import router from './router/index'
import App from './App.vue'
import Vuetify from 'vuetify/lib'
import 'vuetify/dist/vuetify.min.css';

//Graphql
import apolloClient from './config/index'
import VueApollo from 'vue-apollo'


Vue.use(VueApollo)
Vue.use(Vuetify)


//apolloProvider
const apolloProvider = new VueApollo({
  defaultClient: apolloClient
})


Vue.config.productionTip = false






new Vue({
  el: "#app",
  template: '<App/>',
  router,
  apolloProvider,
  components: {App}

})Finally, we will set up the src/pages/login.vue file, which we should have configured as the component for the default home route ‘/’ in the src/router/index.js file.

<template>
<div>
<div v-show="!graphReady">
<v-form
lazy-validation
v-model="valid"
ref="form"
>
<v-text-field
v-model="email"
label="Email"
required
></v-text-field>
<v-text-field
v-model="password"
type="password"
label="Password"
required
></v-text-field>
<v-btn
:disabled="!valid"
@click="signup"
>
submit
</v-btn>
</v-form>
</div>
<div v-show="graphReady">
<h1>Hey <span v-if="responseMail">{{responseMail.email}},</span></h1>

                &lt;line-chart :chart-data="graphPoints" :options="options" /&gt;
            &lt;/div&gt;
    &lt;/div&gt;
&lt;/template&gt;


&lt;script&gt;
import {IMAP_MUTATION} from '../config/graphql'
import LineChart from '../config/LineChart.js'
import apolloClient from '../config/index'
import moment from 'moment'
import gql from 'graphql-tag'
export default {
    components: {
        LineChart
    },
    data(){
        return {
            valid: true,
            email: null,
            password: null,
            graphReady: false,
            responseMail: null,
            options:{
                legend:{
                    display: false
                },
                responsive: true,
                maintainAspectRatio: false,
                scales:{
                    xAxes:[{
                        gridLines:{
                            display: true,
                            lineWidth: 1,
                            drawBorder: false
                        }
                    }],
                    yAxes:[{
                        gridLines:{
                            display: true,
                            lineWidth: 3,
                            drawBorder: false
                        }
                    }]
                }
            },
            graphPoints: null


        }
    },
    methods: {
        signup(){
            if (this.$refs.form.validate()) {
                /*Include no-cache policy for this route
                  to avoid caching passwords */
                this.$apollo
                    .mutate({
                        mutation: IMAP_MUTATION,
                        variables:{
                            email: this.email,
                            password: this.password
                        }
                        , fetchPolicy: 'no-cache'
                    })
                    .then(response =&gt; {
                        this.snackbar = false;
                       
                        this.responseMail = response.data.imapMutation;
                        this.graphReady = true;
                        return this.responseMail
                    })
                    .then(res =&gt;{ this.plotGraph() })
                    .catch(e =&gt;{ console.log(e) })
            }    
        },
        plotGraph(){
            this.graphPoints = {
                labels:[],
                datasets: [
                    {
                        label: 'Medium followers',
                        borderColor: 'green',
                        fill:false,
                        lintTension: 0,
                        pointBackgroundColor: 'red',
                        data:[]
                    }
                ]
            }
            if(this.responseMail){  
                this.responseMail.data.map(point =&gt;{
                    this.graphPoints.labels.push(this.formatDate(point.date.split('T')[0]))
                    this.graphPoints.datasets[0].data.push(point.numberOfFollowers)
                  
                })
            }
        },
        formatDate(date){
            return moment(date, 'YYYY-MM-DD').format('Do MMM YY')
        }
    }
}
&lt;/script&gt;

So we created a basic form that accepts the email and password of the user and then send this data to the server through the signup() method. The plotGraph() method loops through the response(graphPoints) and pushes the dates to the labels array and the number of followers to the data array.

After styling using the ‘options’ object, we should have something like the screenshot shown during the introduction to this project.

Conclusion

You can still do more with your personal project, but the aim of this was to show how to work with the node-imap package, and how apollo works as a server and as a client. If you had any problems with this project, you can leave a comment or send a message on Twitter.


You can try the live version of this, or you could check out the repositories for the client and server applications on GitHub.

If you want to learn new technologies and frameworks, you can do so hereand if you learned anything at all, please do well to clap below.

So we created a basic form that accepts the email and password of the user and then send this data to the server through the signup() method. The plotGraph() method loops through the response(graphPoints) and pushes the dates to the labels array and the number of followers to the data array.

After styling using the ‘options’ object, we should have something like the screenshot shown during the introduction to this project.

Conclusion

You can still do more with your personal project, but the aim of this was to show how to work with the node-imap package, and how apollo works as a server and as a client. If you had any problems with this project, you can leave a comment or send a message on Twitter.


You can try the live version of this, or you could check out the repositories for the client and server applications on GitHub.

If you want to learn new technologies and frameworks, you can do so hereand if you learned anything at all, please do well to clap below.

I will like to thank Wes Wagner for his feedback in making this article.

Thanks!


By : Paschal



Node, Express, PostgreSQL, Vue 2 and GraphQL CRUD Web App

Node, Express, PostgreSQL, Vue 2 and GraphQL CRUD Web App

A comprehensive step by step tutorial on building CRUD Web App using Node, Express, PostgreSQL, Vue 2 and Graphql CRUD Web App

A comprehensive step by step tutorial on building CRUD Web App using Node, Express, PostgreSQL, Vue 2 and Graphql CRUD Web App

For the client side (Vue 2) we will use Vue-Apollo module. For the backend side, we will use Node, Express, Sequelize, and PostgreSQL with Express-Graphql module and their dependencies. The scenario for this tutorial is simple as usual, just the CRUD operation which data accessible through GraphQL.

Table of Contents:
  • Create Express.js Application and Install Required Modules
  • Add and Configure Sequelize.js Module and Dependencies
  • Create or Generate Models and Migrations
  • Install GraphQL Modules and Dependencies
  • Create GraphQL Schemas for the Book
  • Add Mutation for CRUD Operation to the Schema
  • Test GraphQL using GraphiQL
  • Create Vue 2 Application
  • Install Required Modules, Dependencies, and Router
  • Create a Component to Display List of Books
  • Create a Component to Show and Delete Books
  • Create a Component to Add a New Book
  • Create a Component to Edit a Book
  • Run and Test GraphQL CRUD from the Vue 2 Application

The following tools, frameworks, and modules are required for this tutorial:

  • Node.js (choose recommended version)
  • Vue 2
  • Express.js
  • GraphQL
  • Express-GraphQL
  • Vue-Apollo
  • Bootstrap-Vue
  • Terminal (Mac/Linux) or Node Command Line (Windows)
  • IDE or Text Editor (We are using Visual Studio Code)

We assume that you have already Installed Node.js. Make sure Node.js command line is working (on Windows) or runnable in Linux/OS X terminal.

node -v
v10.15.1
npm -v
6.8.0
yarn -v
1.10.1

That the versions that we are uses. Let's continue with the main steps.

1. Create Express.js Application and Install Required Modules

Open your terminal or node command line the go to your projects folder. First, install express generator using this command.

sudo npm install express-generator -g

Next, create an Express.js app using this command.

express vue-graphql

This will create Express.js project with files and directories.

create : vue-graphql/
create : vue-graphql/public/
create : vue-graphql/public/javascripts/
create : vue-graphql/public/images/
create : vue-graphql/public/stylesheets/
create : vue-graphql/public/stylesheets/style.css
create : vue-graphql/routes/
create : vue-graphql/routes/index.js
create : vue-graphql/routes/users.js
create : vue-graphql/views/
create : vue-graphql/views/error.jade
create : vue-graphql/views/index.jade
create : vue-graphql/views/layout.jade
create : vue-graphql/app.js
create : vue-graphql/package.json
create : vue-graphql/bin/
create : vue-graphql/bin/www

Next, go to the newly created project folder then install node modules.

cd vue-graphql && npm install

There's no view yet using the latest Express generator. We don't need it because we will create a GraphQL server.

2. Add and Configure Sequelize.js Module and Dependencies

Before installing the modules for this project, first, install Sequelize-CLI by type this command.

sudo npm install -g sequelize-cli

To install Sequelize.js module, type this command.

npm install --save sequelize

Then install the module for PostgreSQL.

npm install --save pg pg-hstore

Next, create a new file at the root of the project folder.

touch .sequelizerc

Open and edit that file then add these lines of codes.

const path = require('path');

module.exports = {
  "config": path.resolve('./config', 'config.json'),
  "models-path": path.resolve('./models'),
  "seeders-path": path.resolve('./seeders'),
  "migrations-path": path.resolve('./migrations')
};

That files will tell Sequelize initialization to generate config, models, seeders and migrations files to specific directories. Next, type this command to initialize the Sequelize.

sequelize init

That command will create config/config.json, models/index.js, migrations and seeders directories and files. Next, open and edit config/config.json then make it like this.

{
  "development": {
    "username": "djamware",
    "password": "[email protected]@r3",
    "database": "node_sequelize",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "test": {
    "username": "root",
    "password": "[email protected]@r3",
    "database": "node_sequelize",
    "host": "127.0.0.1",
    "dialect": "postgres"
  },
  "production": {
    "username": "root",
    "password": "[email protected]@r3",
    "database": "node_sequelize",
    "host": "127.0.0.1",
    "dialect": "postgres"
  }
}

We use the same configuration for all the environment because we are using the same machine, server, and database for this tutorial.

Before run and test connection, make sure you have created a database as described in the above configuration. You can use the psql command to create a user and database.

psql postgres --u postgres

Next, type this command for creating a new user with password then give access for creating the database.

postgres-# CREATE ROLE djamware WITH LOGIN PASSWORD '[email protected]@r3';
postgres-# ALTER ROLE djamware CREATEDB;

Quit psql then log in again using the new user that previously created.

postgres-# \q
psql postgres -U djamware

Enter the password, then you will enter this psql console.

psql (9.5.13)
Type "help" for help.

postgres=>

Type this command to creating a new database.

postgres=> CREATE DATABASE book_store;

Then give that new user privileges to the new database then quit the psql.

postgres=> GRANT ALL PRIVILEGES ON DATABASE book_store TO djamware;
postgres=> \q

3. Create or Generate Models and Migrations

We will use Sequelize-CLI to generate a new model. Type this command to create a model for 'Book'.

sequelize model:generate --name Book --attributes isbn:string,title:string,author:string,description:string,publishedYear:integer,publisher:string

That commands will generate models and migration files. The content of the model file looks like this.

'use strict';
module.exports = (sequelize, DataTypes) => {
  const Book = sequelize.define('Book', {
    isbn: DataTypes.STRING,
    title: DataTypes.STRING,
    author: DataTypes.STRING,
    description: DataTypes.STRING,
    publishedYear: DataTypes.INTEGER,
    publisher: DataTypes.STRING
  }, {});
  Book.associate = function(models) {
    // associations can be defined here
  };
  return Book;
};

And the migration file looks like this.

'use strict';
module.exports = {
  up: (queryInterface, Sequelize) => {
    return queryInterface.createTable('Books', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      isbn: {
        type: Sequelize.STRING
      },
      title: {
        type: Sequelize.STRING
      },
      author: {
        type: Sequelize.STRING
      },
      description: {
        type: Sequelize.STRING
      },
      publishedYear: {
        type: Sequelize.INTEGER
      },
      publisher: {
        type: Sequelize.STRING
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: (queryInterface, Sequelize) => {
    return queryInterface.dropTable('Books');
  }
};

Finally, for migrations, there's nothing to change and they all ready to generate the table to the PostgreSQL Database. Type this command to generate the table to the database.

sequelize db:migrate

4. Install GraphQL Modules and Dependencies

Now, the GraphQL time. Type this command to install GraphQL modules and it's dependencies.

npm install express express-graphql graphql graphql-date cors --save

Next, open and edit app.js then declare all of those modules and dependencies.

var graphqlHTTP = require('express-graphql');
var schema = require('./graphql/bookSchemas');
var cors = require("cors");

The schema is not created yet, we will create it in the next steps. Next, add these lines of codes for configuring GraphQL that can use over HTTP.

app.use('*', cors());
app.use('/graphql', cors(), graphqlHTTP({
  schema: schema,
  rootValue: global,
  graphiql: true,
}));

That's configuration are enabled CORS and the GraphiQL. GraphiQL is the user interface for testing GraphQL query.

5. Create GraphQL Schemas for the Book

Create a folder at the server folder for hold GraphQL Schema files then create a Javascript file for the schema.

mkdir graphql
touch graphql/bookSchemas.js

Next, open and edit server/graphql/bookSchemas.js then declares all required modules and models.

var GraphQLSchema = require('graphql').GraphQLSchema;
var GraphQLObjectType = require('graphql').GraphQLObjectType;
var GraphQLList = require('graphql').GraphQLList;
var GraphQLObjectType = require('graphql').GraphQLObjectType;
var GraphQLNonNull = require('graphql').GraphQLNonNull;
var GraphQLID = require('graphql').GraphQLID;
var GraphQLString = require('graphql').GraphQLString;
var GraphQLInt = require('graphql').GraphQLInt;
var GraphQLDate = require('graphql-date');
var BookModel = require('../models').Book;

Create a GraphQL Object Type for Book models.

var bookType = new GraphQLObjectType({
  name: "book",
  fields: function() {
    return {
      id: {
        type: GraphQLInt
      },
      isbn: {
        type: GraphQLString
      },
      title: {
        type: GraphQLString
      },
      author: {
        type: GraphQLString
      },
      description: {
        type: GraphQLString
      },
      publishedYear: {
        type: GraphQLInt
      },
      publisher: {
        type: GraphQLString
      },
      createdAt: {
        type: GraphQLDate
      },
      updatedAt: {
        type: GraphQLDate
      }
    };
  }
});

Next, create a GraphQL query type that calls a list of book and single book by ID.

var queryType = new GraphQLObjectType({
  name: 'Query',
  fields: function () {
    return {
      books: {
        type: new GraphQLList(bookType),
        resolve: function () {
          const books = BookModel.findAll({
            order: [
              ['createdAt', 'DESC']
            ],
          })
          if (!books) {
            throw new Error('Error')
          }
          return books
        }
      },
      book: {
        type: bookType,
        args: {
          id: {
            name: 'id',
            type: GraphQLString
          }
        },
        resolve: function (root, params) {
          const bookDetails = BookModel.findByPk(params.id).exec()
          if (!bookDetails) {
            throw new Error('Error')
          }
          return bookDetails
        }
      }
    }
  }
});

Finally, exports this file as GraphQL schema by adding this line at the end of the file.

module.exports = new GraphQLSchema({query: queryType});

6. Add Mutation for CRUD Operation to the Schema

For completing CRUD (Create, Read, Update, Delete) operation of the GraphQL, we need to add a mutation that contains create, update and delete operations. Open and edit graphql/bookSchemas.js then add this mutation as GraphQL Object Type.

var mutation = new GraphQLObjectType({
  name: 'Mutation',
  fields: function () {
    return {
      addBook: {
        type: bookType,
        args: {
          isbn: {
            type: new GraphQLNonNull(GraphQLString)
          },
          title: {
            type: new GraphQLNonNull(GraphQLString)
          },
          author: {
            type: new GraphQLNonNull(GraphQLString)
          },
          description: {
            type: new GraphQLNonNull(GraphQLString)
          },
          publishedYear: {
            type: new GraphQLNonNull(GraphQLInt)
          },
          publisher: {
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve: function (root, params) {
          const bookModel = new BookModel(params);
          const newBook = bookModel.save();
          if (!newBook) {
            throw new Error('Error');
          }
          return newBook
        }
      },
      updateBook: {
        type: bookType,
        args: {
          id: {
            name: 'id',
            type: new GraphQLNonNull(GraphQLInt)
          },
          isbn: {
            type: new GraphQLNonNull(GraphQLString)
          },
          title: {
            type: new GraphQLNonNull(GraphQLString)
          },
          author: {
            type: new GraphQLNonNull(GraphQLString)
          },
          description: {
            type: new GraphQLNonNull(GraphQLString)
          },
          publishedYear: {
            type: new GraphQLNonNull(GraphQLInt)
          },
          publisher: {
            type: new GraphQLNonNull(GraphQLString)
          }
        },
        resolve(root, params) {
          return BookModel
          .findByPk(params.id)
          .then(book => {
            if (!book) {
              throw new Error('Not found');
            }
            return book
              .update({
                isbn: params.isbn || book.isbn,
                title: params.title || book.title,
                author: params.author || book.author,
                description: params.description || book.description,
                publishedYear: params.publishedYear || book.publishedYear,
                publisher: params.publisher || book.publisher,
              })
              .then(() => { return book; })
              .catch((error) => { throw new Error(error); });
          })
          .catch((error) => { throw new Error(error); });
        }
      },
      removeBook: {
        type: bookType,
        args: {
          id: {
            type: new GraphQLNonNull(GraphQLInt)
          }
        },
        resolve(root, params) {
          return BookModel
          .findByPk(params.id)
          .then(book => {
            if (!book) {
              throw new Error('Not found');
            }
            return book
              .destroy()
              .then(() => { return book; })
              .catch((error) => { throw new Error(error); });
          })
          .catch((error) => { throw new Error(error); });
        }
      }
    }
  }
});

Finally, add this mutation to the GraphQL Schema exports like below.

module.exports = new GraphQLSchema({query: queryType, mutation: mutation});

7. Test GraphQL using GraphiQL

To test the queries and mutations of CRUD operations, re-run again the Express.js app then open the browser. Go to this address [http://localhost:3000/graphql](http://localhost:3000/graphql "http://localhost:3000/graphql") to open the GraphiQL User Interface.

To get the list of books, replace all of the text on the left pane with this GraphQL query then click the Play button.

To get a single book by ID, use this GraphQL query.

{
  book(id: 1) {
    id
    isbn
    title
    author
    description
    publishedYear
    publisher
    updatedAt
  }
}

To add a book, use this GraphQL mutation.

mutation {
  addBook(
    isbn: "12345678",
    title: "Whatever this Book Title",
    author: "Mr. Bean",
    description: "The short explanation of this Book",
    publisher: "Djamware Press",
    publishedYear: 2019
  ) {
    updatedAt
  }
}

You will the response at the right pane like this.

{
  "data": {
    "addBook": {
      "updatedAt": "2019-02-26T13:55:39.160Z"
    }
  }
}

To update a book, use this GraphQL mutation.

mutation {
  updateBook(
    id: 1,
    isbn: "12345678221",
    title: "The Learning Curve of GraphQL",
    author: "Didin J.",
    description: "The short explanation of this Book",
    publisher: "Djamware Press",
    publishedYear: 2019
  ) {
    id,
    updatedAt
  }
}

You will see the response in the right pane like this.

{
  "data": {
    "updateBook": {
      "id": 1,
      "updated_date": "2019-02-26T13:58:35.811Z"
    }
  }
}

To delete a book by ID, use this GraphQL mutation.

mutation {
  removeBook(id: 1) {
    id
  }
}

You will see the response in the right pane like this.

{
  "data": {
    "removeBook": {
      "id": 1
    }
  }
}

8. Create Vue 2 Application

To install Vue-CLI type this command from the Terminal or Node command line.

sudo npm install -g @vue/cli

or

yarn global add @vue/cli

Next, check the version to make sure that you have the 3.x version of Vue-CLI.

vue --version
3.7.0

Next, create a new Vue.js project by type this command.

vue create client

For now, use the default for every question that shows up in the Terminal. Next, go to the newly created folder.

cd ./client

To make sure that created Vue.js project working, type this command to run the Vue.js application.

npm run serve

or

yarn serve

You will see this page when open [http://localhost:8080/](http://localhost:8080/ "http://localhost:8080/") in the browser.

9. Install/Configure the Required Modules, Dependencies, and Router

Now, we have to install and configure all of the required modules and dependencies. Type this command to install the modules.

npm install apollo-boost vue-apollo graphql-tag graphql vue-router --save

Next, open and edit src/main.js then add these imports.

import ApolloClient from "apollo-boost";
import VueApollo from "vue-apollo";

Add these constant variables then register VueApollo in Vue 2 app.

const apolloClient = new ApolloClient({
  uri: 'http://localhost:3000/graphql'
});

const apolloProvider = new VueApollo({
  defaultClient: apolloClient
});

Vue.use(VueApollo);

new Vue({
  apolloProvider,
  render: h => h(App)
}).$mount('#app')

To register or create routes for the whole application navigation, create a router folder and index.js file.

mkdir src/router
touch src/router/index.js

Open and edit src/router/index.js then add these imports.

import VueRouter from 'vue-router'
import BookList from '@/components/BookList'
import ShowBook from '@/components/ShowBook'
import AddBook from '@/components/AddBook'
import EditBook from '@/components/EditBook'

Add the router to each component or page.

export default new VueRouter({
  routes: [
    {
      path: '/',
      name: 'BookList',
      component: BookList
    },
    {
      path: '/show-book/:id',
      name: 'ShowBook',
      component: ShowBook
    },
    {
      path: '/add-book',
      name: 'AddBook',
      component: AddBook
    },
    {
      path: '/edit-book/:id',
      name: 'EditBook',
      component: EditBook
    }
  ]
})

Add Vue files for above-registered components or pages.

touch src/components/BookList.vue
touch src/components/ShowBook.vue
touch src/components/AddBook.vue
touch src/components/EditBook.vue

Finally, add or register this router file to src/main.js by adding these imports.

import VueRouter from 'vue-router'
import router from './router'

Register the Vue-Router after Vue.config.

Vue.use(VueRouter)

Modify new Vue to be like this.

new Vue({
  apolloProvider,
  router,
  render: h => h(App)
}).$mount('#app')

10. Create a Component to Display List of Books

Before create or show data to the views, we have to add Bootstrap-Vue. Type this command to install the module.

npm i bootstrap-vue

Next, open and edit src/main.js then add these imports.

import BootstrapVue from 'bootstrap-vue'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Add this line after Vue.config.

Vue.use(BootstrapVue);

Now, open and edit src/components/BookList.vue then add this template tags that contain a bootstrap-vue table.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Book List
        <b-link href="#/add-Book">(Add Book)</b-link>
      </h2>
      <b-table striped hover :items="books" :fields="fields">
        <template slot="actions" scope="row">
          <b-btn size="sm" @click.stop="details(row.item)">Details</b-btn>
        </template>
      </b-table>
    </b-col>
  </b-row>
</template>

Next, add the script tag for hold all Vue 2 codes.

<script></script>

Inside the script tag, add these imports.

import gql from "graphql-tag";
import router from "../router";

Declare the constant variables for GraphQL query.

const GET_BOOKS = gql`
  {
    books {
      id
      title
      author
    }
  }
`;

Add the main Vue 2 export that contains Vue-Apollo calls that filled Vue 2 data.

export default {
  name: "BookList",
  apollo: {
    books: {
      query: GET_BOOKS,
      pollInterval: 300
    }
  },
  data() {
    return {
      fields: {
        title: { label: "Title", sortable: true, class: "text-left" },
        author: { label: "Author", sortable: true, class: "text-left" },
        actions: { label: "Action", class: "text-center" }
      },
      books: []
    };
  },
  methods: {
    details(book) {
      router.push({ name: "ShowBook", params: { id: book.id } });
    }
  }
};

Finally, add the style tag for styling the template.

<style>
.table {
  width: 96%;
  margin: 0 auto;
}
</style>

11. Create a Component to Show and Delete Books

To show the book details that contains all book detail, edit and delete buttons, open and edit src/components/ShowBook.vue then add these template tags that contain a bootstrap-vue component for display the details.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Book List
        <b-link href="#/">(Book List)</b-link>
      </h2>
      <b-jumbotron>
        <template slot="header">{{book.title}}</template>
        <template slot="lead">
          ISBN: {{book.isbn}}
          <br>
          Author: {{book.author}}
          <br>
          Description: {{book.description}}
          <br>
          Published Year: {{book.publishedYear}}
          <br>
          Publisher: {{book.publisher}}
          <br>
          Update At: {{book.updatedAt}}
          <br>
        </template>
        <hr class="my-4">
        <b-btn class="edit-btn" variant="success" @click.stop="editBook(book.id)">Edit</b-btn>
        <b-btn variant="danger" @click.stop="deleteBook(book.id)">Delete</b-btn>
      </b-jumbotron>
    </b-col>
  </b-row>
</template>

Next, add the script tag.

<script></script>

Inside the script tag, add these imports.

import gql from "graphql-tag";
import router from "../router";

Declare the constant variables that handle get a single book and delete book queries.

const GET_BOOK = gql`
  query book($bookId: Int) {
    book(id: $bookId) {
      id
      isbn
      title
      author
      description
      publishedYear
      publisher
      updatedAt
    }
  }
`;

const DELETE_BOOK = gql`
  mutation removeBook($id: Int!) {
    removeBook(id: $id) {
      id
    }
  }
`;

Inside main Vue export, add all required functions, variables, and Vue-Apollo function.

export default {
  name: "ShowBook",
  data() {
    return {
      book: '',
      bookId: parseInt(this.$route.params.id)
    };
  },
  apollo: {
    book: {
      query: GET_BOOK,
      pollInterval: 300,
      variables() {
        return {
          bookId: this.bookId
        };
      }
    }
  },
  methods: {
    editBook(id) {
      router.push({
        name: "EditBook",
        params: { id: id }
      });
    },
    deleteBook(id) {
      this.$apollo
        .mutate({
          mutation: DELETE_BOOK,
          variables: {
            id: id
          }
        })
        .then(data => {
          console.log(data);
        })
        .catch(error => {
          console.error(error);
        });
    }
  }
};

Finally, add the style tags to give the view some styles.

<style>
.jumbotron {
  padding: 2rem;
}
.edit-btn {
  margin-right: 20px;
  width: 70px;
}
</style>

12. Create a Component to Add a New Book

To add a new book, open and edit src/components/AddBook.vue then add this Vue 2 template tag that contains a bootstrap-vue form.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Add Book
        <b-link href="#/">(Book List)</b-link>
      </h2>
      <b-jumbotron>
        <b-form @submit="onSubmit">
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter ISBN"
          >
            <b-form-input id="isbn" v-model.trim="book.isbn"></b-form-input>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Title"
          >
            <b-form-input id="title" v-model.trim="book.title"></b-form-input>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Author"
          >
            <b-form-input id="author" v-model.trim="book.author"></b-form-input>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Description"
          >
            <b-form-textarea
              id="description"
              v-model="book.description"
              placeholder="Enter something"
              :rows="2"
              :max-rows="6"
            >{{book.description}}</b-form-textarea>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Publisher"
          >
            <b-form-input id="publisher" v-model.trim="book.publisher"></b-form-input>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Published Year"
          >
            <b-form-input type="number" id="publishedYear" v-model.trim="book.publishedYear"></b-form-input>
          </b-form-group>
          <b-button type="submit" variant="primary">Save</b-button>
        </b-form>
      </b-jumbotron>
    </b-col>
  </b-row>
</template>

Next, add the script tag.

<script></script>

Inside the script, tag adds Vue 2 codes that contain Vue-Apollo GraphQL mutation to save a new book.

import gql from "graphql-tag";
import router from "../router";

const ADD_BOOK = gql`
  mutation AddBook(
    $isbn: String!
    $title: String!
    $author: String!
    $description: String!
    $publisher: String!
    $publishedYear: Int!
  ) {
    addBook(
      isbn: $isbn
      title: $title
      author: $author
      description: $description
      publisher: $publisher
      publishedYear: $publishedYear
    ) {
      id
    }
  }
`;

export default {
  name: "AddBook",
  data() {
    return {
      book: {}
    };
  },
  methods: {
    onSubmit(evt) {
      evt.preventDefault();

      this.$apollo
        .mutate({
          mutation: ADD_BOOK,
          variables: {
            isbn: this.book.isbn,
            title: this.book.title,
            author: this.book.author,
            description: this.book.description,
            publisher: this.book.publisher,
            publishedYear: parseInt(this.book.publishedYear)
          }
        })
        .then(data => {
          console.log(data);
          router.push({ name: "BookList" });
        })
        .catch(error => {
          console.error(error);
        });
    }
  }
};

Finally, give the view a style by adding the style tag.

<style>
.jumbotron {
  padding: 2rem;
}
</style>

13. Create a Component to Edit a Book

To edit a book after getting single book data, open and edit src/components/EditBook.vue then add this Vue 2 template that contains a bootstrap-vue form.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Edit Book
        <router-link :to="{ name: 'ShowBook', params: { id: bookId } }">(Show Book)</router-link>
      </h2>
      <b-jumbotron>
        <b-form @submit="onSubmit">
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter ISBN"
          >
            <b-form-input id="isbn" v-model.trim="book.isbn"></b-form-input>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Title"
          >
            <b-form-input id="title" v-model.trim="book.title"></b-form-input>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Author"
          >
            <b-form-input id="author" v-model.trim="book.author"></b-form-input>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Description"
          >
            <b-form-textarea
              id="description"
              v-model="book.description"
              placeholder="Enter something"
              :rows="2"
              :max-rows="6"
            >{{book.description}}</b-form-textarea>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Publisher"
          >
            <b-form-input id="publisher" v-model.trim="book.publisher"></b-form-input>
          </b-form-group>
          <b-form-group
            id="fieldsetHorizontal"
            horizontal
            :label-cols="4"
            breakpoint="md"
            label="Enter Published Year"
          >
            <b-form-input type="number" id="publishedYear" v-model.trim="book.publishedYear"></b-form-input>
          </b-form-group>
          <b-button type="submit" variant="primary">Update</b-button>
        </b-form>
      </b-jumbotron>
    </b-col>
  </b-row>
</template>

Next, add the script tag that contains all required Vue 2 codes with get data and update function.

<script>
import gql from "graphql-tag";
import router from "../router";

const GET_BOOK = gql`
  query book($bookId: Int) {
    book(id: $bookId) {
      id
      isbn
      title
      author
      description
      publishedYear
      publisher
    }
  }
`;

const UPDATE_BOOK = gql`
  mutation updateBook(
    $id: Int!
    $isbn: String!
    $title: String!
    $author: String!
    $description: String!
    $publisher: String!
    $publishedYear: Int!
  ) {
    updateBook(
      id: $id
      isbn: $isbn
      title: $title
      author: $author
      description: $description
      publisher: $publisher
      publishedYear: $publishedYear
    ) {
      updatedAt
    }
  }
`;

export default {
  name: "EditBook",
  data() {
    return {
      bookId: this.$route.params.id,
      book: {}
    };
  },
  apollo: {
    book: {
      query: GET_BOOK,
      variables() {
        return {
          bookId: this.bookId
        };
      }
    }
  },
  methods: {
    onSubmit(evt) {
      evt.preventDefault();

      this.$apollo
        .mutate({
          mutation: UPDATE_BOOK,
          variables: {
            id: parseInt(this.book.id),
            isbn: this.book.isbn,
            title: this.book.title,
            author: this.book.author,
            description: this.book.description,
            publisher: this.book.publisher,
            publishedYear: parseInt(this.book.publishedYear)
          }
        })
        .then(data => {
          console.log(data);
          router.push({
            name: "ShowBook",
            params: { id: this.$route.params.id }
          });
        })
        .catch(error => {
          console.error(error);
        });
    }
  }
};
</script>

Finally, give the view some style by adding the style tag.

<style>
.jumbotron {
  padding: 2rem;
}
</style>

14. Run and Test GraphQL CRUD from the Vue 2 Application

We assume the PostgreSQL server already running, so you just can run Node/Express.js application and Vue 2 app in the separate terminal tabs.

nodemon
cd client
npm run serve

Next, open the browser then go to this address localhost:8080 and you should see these pages.

That it's, the Node, Express, PostgreSQL, Vue 2 and Graphql CRUD Web App. You can find the full source code on our GitHub.

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.