Mongo Express Vue Node.js (MEVN Stack) CRUD Web Application

by Didin J. on Nov 27, 2017 Mongo Express Vue Node.js (MEVN Stack) CRUD Web Application

Step by step tutorial of building Mongo, Express.js, Vue.js 2 and Node.js (MEVN Stack) create-read-update-delete (CRUD) web application.

A comprehensive step by step tutorial of building Mongo, Express, Vue.js 2 and Node.js (MEVN Stack) create-read-update-delete (CRUD) web application. Vue.js recently become a top Javascript framework along with Angular and React.js. So, we interesting to make a little experiment to integrate or create a full-stack web application based on Node.js, Express, and MongoDB. Previously, we have done with MEAN Stack (Angular 5) and MERN Stack (React.js).

A shortcut to the steps:

Actually, MEVN is a new term that unknown where it comes. We have used this MEVN terms because there are a lot of Vue.js and full-stack developer using than the name for this MongoDB, Express.js, Vue.js, and Node.js stack. And now, it becomes trends based on Google trends.

Now, we have to make a similar concept to make Vue.js works with Express.js. As usual, we use Express.js as backend and REST API server and Vue.js as Frontend. The following tools, frameworks, and modules are required for this tutorial:

  1. Node.js (use recommended version)
  2. Express.js
  3. MongoDB
  4. Mongoose.js
  5. Vue.js
  6. Vue-CLI
  7. IDE or Text Editor (we use Atom)

We assume that you have already installed Node.js and able to run Node.js command line (Windows) or `npm` on the terminal (MAC/Linux). Open the terminal or Node command line then type this command to install `vue-cli`.

sudo npm install -g vue-cli

That where we start the tutorial. We will create the MEVN stack CRUD web application from `vue-cli`.


Create a New Vue.js Application

To create a new Vue.js application using `vue-cli` simply type this command from terminal or Node command line.

vue init webpack mevn-stack

Next, go to the newly created Vue.js project folder then install all default required modules by type this command.

cd ./mevn-stack
npm install

Now, check the Vue.js application by running the application using this command.

npm run dev

You should see this page when everything still on the track.

Mongo Express Vue Node.js (MEVN Stack) CRUD Web Application - Vue.js 2 home


Install Express.js as REST API Server

Close the running Vue.js app first by press `ctrl+c` then type this command for adding Express.js modules and its dependencies.

npm install --save express body-parser morgan body-parser serve-favicon

Next, create a new folder called `bin` then add a file called `www` on the root of Vue.js project folder.

mkdir bin
touch bin/www

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

#!/usr/bin/env node

/**
 * Module dependencies.
 */

var app = require('../app');
var debug = require('debug')('mean-app:server');
var http = require('http');

/**
 * Get port from environment and store in Express.
 */

var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);

/**
 * Create HTTP server.
 */

var server = http.createServer(app);

/**
 * Listen on provided port, on all network interfaces.
 */

server.listen(port);
server.on('error', onError);
server.on('listening', onListening);

/**
 * Normalize a port into a number, string, or false.
 */

function normalizePort(val) {
  var port = parseInt(val, 10);

  if (isNaN(port)) {
    // named pipe
    return val;
  }

  if (port >= 0) {
    // port number
    return port;
  }

  return false;
}

/**
 * Event listener for HTTP server "error" event.
 */

function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }

  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;

  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}

/**
 * Event listener for HTTP server "listening" event.
 */

function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}

Next, change the default server what run by `npm` command. Open and edit `package.json` then replace `start` value inside `scripts`.

"scripts": {
  "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
  "start": "npm run build && node ./bin/www",
  "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run",
  "e2e": "node test/e2e/runner.js",
  "test": "npm run unit && npm run e2e",
  "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs",
  "build": "node build/build.js"
},

Now, create `app.js` in the root of the project folder.

touch app.js

Open and edit `app.js` then add these lines of codes.

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var bodyParser = require('body-parser');

var book = require('./routes/book');
var app = express();

app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({'extended':'false'}));
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/books', express.static(path.join(__dirname, 'dist')));
app.use('/book', book);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

Next, create routes folder then create routes file for the book.

mkdir routes
touch routes/book.js

Open and edit `routes/book.js` file then add these lines of codes.

var express = require('express');
var router = express.Router();

/* GET home page. */
router.get('/', function(req, res, next) {
  res.send('Express RESTful API');
});

module.exports = router;

Now, run the server using this command.

npm start

You will see the previous Vue.js landing page when you point your browser to `http://localhost:3000`. When you change the address to `http://localhost:3000/book` you will see this page.

Mongo Express Vue Node.js (MEVN Stack) CRUD Web Application - Express Restful API

If you find this error.

Error: No default engine was specified and no extension was provided.

Then add to `app.js` this line after `app.use`.

app.set('view engine', 'html');

And this is the new folders and files structure of Express.js and Vue.js web application.

|-- README.md
|-- app.js
|-- bin
|   `-- www
|-- build
|-- config
|-- dist
|   |-- index.html
|   `-- static
|       |-- css
|       `-- js
|-- index.html
|-- node_modules
|-- package.json
|-- routes
|-- src
|-- static
`-- test
    |-- e2e
    `-- unit


Install and Configure Mongoose.js

We need to access data from MongoDB. For that, we will install and configure Mongoose.js. On the terminal type this command after stopping the running Express server.

npm install --save mongoose bluebird

Open and edit `app.js` then add these lines after another variable line.

var mongoose = require('mongoose');
mongoose.Promise = require('bluebird');
mongoose.connect('mongodb://localhost/mean-angular5', { useMongoClient: true, promiseLibrary: require('bluebird') })
  .then(() =>  console.log('connection succesful'))
  .catch((err) => console.error(err));

Now, run the MongoDB server on different terminal tab or command line or run from the service.

mongod

Next, you can test the connection to MongoDB run again the Node application and you will see this message on the terminal.

connection succesful

If you are still using built-in Mongoose Promise library, you will get this deprecated warning on the terminal.

(node:42758) DeprecationWarning: Mongoose: mpromise (mongoose's default promise library) is deprecated, plug in your own promise library instead: http://mongoosejs.com/docs/promises.html

That's the reason why we added `bluebird` modules and register it as Mongoose Promise library.


Create Mongoose.js Model

Add a models folder on the root of the project folder for hold Mongoose.js model files.

mkdir models

Create new Javascript file that uses for Mongoose.js model. We will create a model of Book collection.

touch models/Book.js

Now, open and edit that file and add Mongoose require.

var mongoose = require('mongoose');

Then add model fields like this.

var BookSchema = new mongoose.Schema({
  isbn: String,
  title: String,
  author: String,
  description: String,
  published_year: String,
  publisher: String,
  updated_date: { type: Date, default: Date.now },
});

That Schema will mapping to MongoDB collections called book. If you want to know more about Mongoose Schema Datatypes you can find it here http://mongoosejs.com/docs/schematypes.html. Next, export that schema.

module.exports = mongoose.model('Book', BookSchema);


Create Routes for Accessing Book Data via REST API

Open and edit again "routes/book.js” then replace all codes with this.

var express = require('express');
var router = express.Router();
var mongoose = require('mongoose');
var Book = require('../models/Book.js');

/* GET ALL BOOKS */
router.get('/', function(req, res, next) {
  Book.find(function (err, products) {
    if (err) return next(err);
    res.json(products);
  });
});

/* GET SINGLE BOOK BY ID */
router.get('/:id', function(req, res, next) {
  Book.findById(req.params.id, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* SAVE BOOK */
router.post('/', function(req, res, next) {
  Book.create(req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* UPDATE BOOK */
router.put('/:id', function(req, res, next) {
  Book.findByIdAndUpdate(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

/* DELETE BOOK */
router.delete('/:id', function(req, res, next) {
  Book.findByIdAndRemove(req.params.id, req.body, function (err, post) {
    if (err) return next(err);
    res.json(post);
  });
});

module.exports = router;

Run again the Express server then open the other terminal or command line to test the Restful API by type this command.

curl -i -H "Accept: application/json" localhost:3000/book

If that command return response like below then REST API is ready to go.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 2
ETag: W/"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w"
Date: Fri, 10 Nov 2017 23:53:52 GMT
Connection: keep-alive

Now, let's populate Book collection with initial data sent from REST API. Run this command to populate it.

curl -i -X POST -H "Content-Type: application/json" -d '{ "isbn":"211333122, 98872233321123","title":"How to Build MEVN Stack","author": "Didin J.","description":"The comprehensive step by step tutorial on how to build MEVN (MongoDB, Express.js, Vue.js and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com" }' localhost:3000/book

You will see this response to the terminal if success.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 378
ETag: W/"17a-monzg91k25p3x996snFiZXWIP4Q"
Date: Sat, 25 Nov 2017 23:27:18 GMT
Connection: keep-alive

{"__v":0,"isbn":"211333122, 98872233321123","title":"How to Build MEVN Stack","author":"Didin J.","description":"The comprehensive step by step tutorial on how to build MEVN (MongoDB, Express.js, Vue.js and Node.js) stack web application from scratch","published_year":"2017","publisher":"Djamware.com","_id":"5a19fc555819f62adde6d44e","updated_date":"2017-11-25T23:27:17.646Z"}


Create Vue.js Component and Routing

Now, it's time for Vue.js or front end part. First, create or add the component of the book list, show, edit and create. Create all of those files inside the components folder.

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

Now, open and edit `src/router/index.js` then add the import for all the above new components.

import Vue from 'vue'
import Router from 'vue-router'
import BookList from '@/components/BookList'
import ShowBook from '@/components/ShowBook'
import CreateBook from '@/components/CreateBook'
import EditBook from '@/components/EditBook'

Add the router to each component or page.

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


Add Module for REST API Access and Styling UI

Previously, the file for the booklist component is created. For UI or styling, we are using Bootstrap Vue, to install it type this command on the terminal.

npm i bootstrap-vue [email protected]

Open and edit `src/main.js` then add the imports for Bootstrap-Vue including separate CSS files.

import Vue from 'vue'
import BootstrapVue from 'bootstrap-vue'
import App from './App'
import router from './router'
import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Add this line after `Vue.config`.

Vue.use(BootstrapVue)

Next, we are using Axio for accessing REST API provided by Express.js. To install it, in the terminal type this command.

npm install axios --save


Modify Vue.js Component of Book List

Now, open and edit `src/components/BookList.vue` then add these lines of codes that get a list of books from the REST API then display it to the Vue.js template with Boostrap-Vue.

<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>
      <ul v-if="errors && errors.length">
        <li v-for="error of errors">
          {{error.message}}
        </li>
      </ul>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'BookList',
  data () {
    return {
      fields: {
        isbn: { label: 'ISBN', sortable: true, 'class': 'text-center' },
        title: { label: 'Book Title', sortable: true },
        actions: { label: 'Action', 'class': 'text-center' }
      },
      books: [],
      errors: []
    }
  },
  created () {
    axios.get(`http://localhost:3000/book`)
    .then(response => {
      this.books = response.data
    })
    .catch(e => {
      this.errors.push(e)
    })
  },
  methods: {
    details (book) {
      this.$router.push({
        name: 'ShowBook',
        params: { id: book._id }
      })
    }
  }
}
</script>

There are template and script in one file. The template block contains HTML tags. Script block contains variables, page lifecycle and methods or functions.


Modify Vue.js Component of Create Book

Now, open and edit `src/components/CreateBook.vue` then add these lines of codes that show a Bootstrap-Vue form and submit or POST that form to the REST API.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Add Book
        <b-link href="#/">(Book List)</b-link>
      </h2>
      <b-form @submit="onSubmit">
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter ISBN">
          <b-form-input id="isbn" :state="state" 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" :state="state" 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" :state="state" 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 Year">
          <b-form-input id="published_year" :state="state" v-model.trim="book.published_year"></b-form-input>
        </b-form-group>
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter Publisher">
          <b-form-input id="publisher" :state="state" v-model.trim="book.publisher"></b-form-input>
        </b-form-group>
        <b-button type="submit" variant="primary">Save</b-button>
      </b-form>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'CreateBook',
  data () {
    return {
      book: {}
    }
  },
  methods: {
    onSubmit (evt) {
      evt.preventDefault()
      axios.post(`http://localhost:3000/book`, this.book)
      .then(response => {
        this.$router.push({
          name: 'ShowBook',
          params: { id: response.data._id }
        })
      })
      .catch(e => {
        this.errors.push(e)
      })
    }
  }
}
</script>

That code contains the template for book form, the script that contains Vue.js 2 codes for hold book model and methods for saving the book to REST API.


Modify Vue.js Component of Show Book

Open and edit `src/components/ShowBook.vue` then add these lines of codes that get the single book data then display it to the Bootstrap-Vue templates.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Edit Book
        <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.published_year}}<br>
          Publisher: {{book.publisher}}<br>
        </template>
        <hr class="my-4">
        <p>
          Updated Date: {{book.updated_date}}
        </p>
        <b-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>

<script>

import axios from 'axios'

export default {
  name: 'ShowBook',
  data () {
    return {
      book: []
    }
  },
  created () {
    axios.get(`http://localhost:3000/book/` + this.$route.params.id)
    .then(response => {
      this.book = response.data
    })
    .catch(e => {
      this.errors.push(e)
    })
  },
  methods: {
    editbook (bookid) {
      this.$router.push({
        name: 'EditBook',
        params: { id: bookid }
      })
    },
    deletebook (bookid) {
      axios.delete('http://localhost:3000/book/' + bookid)
      .then((result) => {
        this.$router.push({
          name: 'BookList'
        })
      })
      .catch(e => {
        this.errors.push(e)
      })
    }
  }
}
</script>

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

Delete function also includes in this component inside the methods block.


Modify Vue.js Component of Edit Book

For editing book that chooses from the show book page, open and edit `src/components/EditBook.vue` then add these lines of codes that get a book by ID to the Bootstrap-Vue form then update or PUT to the REST API.

<template>
  <b-row>
    <b-col cols="12">
      <h2>
        Edit Book
        <router-link :to="{ name: 'ShowBook', params: { id: book._id } }">(Show Book)</router-link>
      </h2>
      <b-form @submit="onSubmit">
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter ISBN">
          <b-form-input id="isbn" :state="state" 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" :state="state" 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" :state="state" 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 Year">
          <b-form-input id="published_year" :state="state" v-model.trim="book.published_year"></b-form-input>
        </b-form-group>
        <b-form-group id="fieldsetHorizontal"
                  horizontal
                  :label-cols="4"
                  breakpoint="md"
                  label="Enter Publisher">
          <b-form-input id="publisher" :state="state" v-model.trim="book.publisher"></b-form-input>
        </b-form-group>
        <b-button type="submit" variant="primary">Update</b-button>
      </b-form>
    </b-col>
  </b-row>
</template>

<script>

import axios from 'axios'

export default {
  name: 'EditBook',
  data () {
    return {
      book: {}
    }
  },
  created () {
    axios.get(`http://localhost:3000/book/` + this.$route.params.id)
    .then(response => {
      this.book = response.data
    })
    .catch(e => {
      this.errors.push(e)
    })
  },
  methods: {
    onSubmit (evt) {
      evt.preventDefault()
      axios.put(`http://localhost:3000/book/` + this.$route.params.id, this.book)
      .then(response => {
        this.$router.push({
          name: 'ShowBook',
          params: { id: this.$route.params.id }
        })
      })
      .catch(e => {
        this.errors.push(e)
      })
    }
  }
}
</script>

This component is almost the same as creating book component, except for load book data by id and method for update data using `PUT`.


Run The MEVN Stack CRUD Web Application

This time to test all complete the MEVN Stack configuration. Type this command to run again this web application.

npm start

And here the application looks like, you can test all CRUD functionality.

Mongo Express Vue Node.js (MEVN Stack) CRUD Web Application - CRUD flow

That's it, you can find the full source code on our GitHub.

That just the basic. If you need more deep learning about MEVN Stack, Vue.js or related you can take the following cheap course:

Thanks!

Loading…