Building CRUD Web Application using MERN Stack

by Didin J. on Nov 02, 2017 Building CRUD Web Application using MERN Stack

A comprehensive step by step tutorial on building CRUD (Create, Read, Update, and Delete) using MERN (MongoDB, Express.js, React.js, and Node.js) Stack

A comprehensive step by step tutorial of building create-read-update-delete (CRUD) web application from scratch using MERN (Mongo, Express, React, Node.js) stack. One of the most popular web application stack based on Node.js is MERN stack. MERN is the abbreviation of MongoDB, Express.js, React.js, and Node.js, almost the same with the MEAN stack except MEAN stack using Angular and MERN stack using React. This basic step by step tutorial of MERN stack is building CRUD (Create-Read-Update-Delete) web application. You can find basic CRUD tutorial of MEAN Stack here.


Table of Contents:


MERN stack is complete Full-stack Javascript development framework that builds by the combination of the Javascript Web frameworks (Express.js and React.js) technologies, MongoDB Database, and Node.js environment. So, a MERN is short for MongoDB, Express.js, React.js, and Node.js. 

React can integrate with MERN stack as the front end that can access data from Express and MongoDB by consuming RESTful API. In this scenario of MERN Stack React will accessing data from Express and MongoDB by HTTP calls as default React component. We are using the same application for Express and React that run on the same IP and PORT. Before starting the tutorial, the following tools and framework are required:

  1. Node.js (Stable build recommended)
  2. Express.js
  3. MongoDB
  4. Mongoose.js
  5. React.js
  6. Terminal (Linux/Mac OS) or Command Line (Windows)
  7. IDE (Visual Studio Code, Eclipse, Netbeans) or Text Editor

We assume that you have already installed Node.js and MongoDB. Make sure Node.js command line is working (on Windows) or runnable in Linux/OS X terminal. You can run MongoDB in the different terminal or command line.


MERN Stack: Install and Create React App

Open the terminal or Node.js command line then go to your MERN projects folder. We will install React app creator (create-react-app) for creating a React app easily. For that, type this command.

sudo npm install -g create-react-app

Now, create a React app by type this command.

create-react-app mern-crud

This command will create a new React app with the name `mern-crud` and this process can take sometimes because all dependencies and modules also installing automatically. Next, go to the newly created app folder.

cd ./mern-crud

Now, run the React app for the first time using this command.

npm start

It will automatically open the default browser the point to `http://localhost:3000`, so the landing page should be like this.

Building CRUD Web Application using MERN Stack - React Home Page


MERN Stack: Install and Configure Express.js

We have to replace the existing Node server that holds the default React application by Express.js as the backend. Stop the server by push keyboard Ctrl+C. Type this command to install Express.js and required dependencies (body-parser, morgan, body-parser,serve-platform).

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

Create a folder with the name "bin" and add a file with the name "www" on the root of the project. You can use your own folder and file as the starter Node.js file.

mkdir bin
touch bin/www

Open and edit `bin/www` file then add these lines of codes that contain the configuration and initialization of Node application which is similar with generated Express application. They are HTTP server, Port configuration, Error handling, and Port listener.

#!/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);
}

Now, open and edit "package.json" then replace "start" value to run the Node application using `bin/www` file.

"scripts": {
  "start": "node ./bin/www",
  "build": "react-scripts build",
  "test": "react-scripts test --env=jsdom",
  "eject": "react-scripts eject"
}

Next, create "app.js" at the root of the project folder.

touch app.js

Open and edit app.js then add these lines of codes than contain Express configuration, routes initialization, request, response, and error handling.

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, 'build')));

app.use('/api/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;

Create the Express routes for the book model.

mkdir routes
touch routes/book.js

Open and edit “book.js" then add these lines of the GET route that just response a simple text.

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

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

module.exports = router;

Now, open and edit `src/registerServiceWorkers.js` then add to the register function this `fetch` event listener below `load` event listener.

window.addEventListener('fetch', (event) => {
  if ( event.request.url.match( '^.*(\/api\/).*$' ) ) {
    return false;
  }
});

So, the complete register function will look like this.

export default function register() {
  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
    // The URL constructor is available in all browsers that support SW.
    const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
    if (publicUrl.origin !== window.location.origin) {
      // Our service worker won't work if PUBLIC_URL is on a different origin
      // from what our page is served on. This might happen if a CDN is used to
      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
      return;
    }

    window.addEventListener('load', (event) => {
      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;

      if (!isLocalhost) {
        // Is not local host. Just register service worker
        registerValidSW(swUrl);
      } else {
        // This is running on localhost. Lets check if a service worker still exists or not.
        checkValidServiceWorker(swUrl);
      }
    });

    window.addEventListener('fetch', (event) => {
      if ( event.request.url.match( '^.*(\/api\/).*$' ) ) {
        return false;
      }
    });
  }
}

Now, check the configuration by running again the Node server that handles by Express now.

npm start

Open the browser then go to `http://localhost:3000` then you will see the previous React home page. Then change the address to `http://localhost:3000/api/book`. You will see this response of web page.

Building CRUD Web Application using MERN Stack - Express RESTful API

That means it works.

 

MERN Stack: Install and Configure Mongoose

The part of MERN stack is MongoDB, to get connected to MongoDB from Node.js application we are usually using Mongoose.js as MongoDB object modeling. Mongoose provides a straight-forward, schema-based solution to model your application data. It includes built-in type casting, validation, query building, business logic hooks and more, out of the box. On the terminal type this command after stopping the running Express server.

npm install --save mongoose bluebird

Open and edit `app.js` then add this lines after another variable line that declare a connection to MongoDB database using Mongoose.

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

You can test the connection to MongoDB by 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.


MERN Stack: Create Mongoose.js Model

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

mkdir models

Create a new Javascript file that uses for Mongoose.js model or schemas. 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_date: { type: Date },
  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. Next, export that schema.

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


MERN Stack: Create Routes for Accessing Book Data via REST API

We will the Express routes to connecting request from the client via REST API to Mongoose or MongoDB collections or models and return back as the JSON formatted data. 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 REST API by type this command.

curl -i -H "Accept: application/json" localhost:3000/api/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: Sat, 28 Oct 2017 22:39:59 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":"1783551623, 9781783551620","title":"React.js Essentials: A Fast-paced Guide to Designing and Building Scalable and Maintainable Web Apps With React.js","author": "Artemij Fedosejev","description":"A fast-paced guide to designing and building scalable and maintainable web apps with React.js.","published_date":"2015-08-27T16:00:00Z","publisher":"Packt Publishing" }' localhost:3000/api/book

You will see this response on the terminal if success.

HTTP/1.1 200 OK
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 459
ETag: W/"1cb-G9luzsvo07EKg3s00VUyB19WpA4"
Date: Thu, 02 Nov 2017 00:01:22 GMT
Connection: keep-alive

{"__v":0,"isbn":"1783551623, 9781783551620","title":"React.js Essentials: A Fast-paced Guide to Designing and Building Scalable and Maintainable Web Apps With React.js","author":"Artemij Fedosejev","description":"A fast-paced guide to designing and building scalable and maintainable web apps with React.js.","published_date":"2015-08-27T16:00:00.000Z","publisher":"Packt Publishing","_id":"59fa6052e245b0060c9aa5d5","updated_date":"2017-11-02T00:01:22.058Z"}


MERN Stack: Add React Router DOM

Our front end consists of Booklist, detail, create and edit. For navigating between that component, we need to create a route. we will use react-router-dom as DOM bindings for the React Router. First, install modules required by the components.

npm install --save react-router-dom
npm install --save-dev bootstrap
npm install --save axios

Next, open and edit `src/index.js` then replace all codes with this React Router components.

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Route } from 'react-router-dom';
import '../node_modules/bootstrap/dist/css/bootstrap.min.css';
import '../node_modules/bootstrap/dist/css/bootstrap-theme.min.css';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';
import Edit from './components/Edit';
import Create from './components/Create';
import Show from './components/Show';

ReactDOM.render(
  <Router>
      <div>
        <Route exact path='/' component={App} />
        <Route path='/edit/:id' component={Edit} />
        <Route path='/create' component={Create} />
        <Route path='/show/:id' component={Show} />
      </div>
  </Router>,
  document.getElementById('root')
);
registerServiceWorker();

You see that Edit, Create and Show added as the separate component. Bootstrap also included in the import to make the views better. Now, create the new edit, create and show files.

mkdir src/components
touch src/components/Create.js
touch src/components/Show.js
touch src/components/Edit.js


MERN Stack: Add List of Book to Existing App Component

As you see in the previous step that App component act as home or root page. This component handles the list of books. Open and edit `src/App.js` then replace all codes with these codes contain REST API fetch using Axios then display data in the rendered components or templates.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Link } from 'react-router-dom';
import axios from 'axios';

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      books: []
    };
  }

  componentDidMount() {
    axios.get('/api/book')
      .then(res => {
        this.setState({ books: res.data });
        console.log(this.state.books);
      });
  }

  render() {
    return (
      <div class="container">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title">
              BOOK CATALOG
            </h3>
          </div>
          <div class="panel-body">
            <h4><Link to="/create"><span class="glyphicon glyphicon-plus-sign" aria-hidden="true"></span> Add Book</Link></h4>
            <table class="table table-stripe">
              <thead>
                <tr>
                  <th>ISBN</th>
                  <th>Title</th>
                  <th>Author</th>
                </tr>
              </thead>
              <tbody>
                {this.state.books.map(book =>
                  <tr>
                    <td><Link to={`/show/${book._id}`}>{book.isbn}</Link></td>
                    <td>{book.title}</td>
                    <td>{book.author}</td>
                  </tr>
                )}
              </tbody>
            </table>
          </div>
        </div>
      </div>
    );
  }
}

export default App;

The first line of that React codes is the imports of the required React component, ReactDOM, React Router DOM Link, and Axios. Inside that React component class, they state declaration of data, fetch data inside React componentDidMount function, render the data to HTML table component.
 

MERN Stack: Add Create Components for Add New Book

For add a new book, open and edit `src/components/Create.js` then replace all codes with these codes. 

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import { Link } from 'react-router-dom';

class Create extends Component {

  constructor() {
    super();
    this.state = {
      isbn: '',
      title: '',
      author: '',
      description: '',
      published_year: '',
      publisher: ''
    };
  }
  onChange = (e) => {
    const state = this.state
    state[e.target.name] = e.target.value;
    this.setState(state);
  }

  onSubmit = (e) => {
    e.preventDefault();

    const { isbn, title, author, description, published_year, publisher } = this.state;

    axios.post('/api/book', { isbn, title, author, description, published_year, publisher })
      .then((result) => {
        this.props.history.push("/")
      });
  }

  render() {
    const { isbn, title, author, description, published_year, publisher } = this.state;
    return (
      <div class="container">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title">
              ADD BOOK
            </h3>
          </div>
          <div class="panel-body">
            <h4><Link to="/"><span class="glyphicon glyphicon-th-list" aria-hidden="true"></span> Book List</Link></h4>
            <form onSubmit={this.onSubmit}>
              <div class="form-group">
                <label for="isbn">ISBN:</label>
                <input type="text" class="form-control" name="isbn" value={isbn} onChange={this.onChange} placeholder="ISBN" />
              </div>
              <div class="form-group">
                <label for="title">Title:</label>
                <input type="text" class="form-control" name="title" value={title} onChange={this.onChange} placeholder="Title" />
              </div>
              <div class="form-group">
                <label for="author">Author:</label>
                <input type="text" class="form-control" name="author" value={author} onChange={this.onChange} placeholder="Author" />
              </div>
              <div class="form-group">
                <label for="description">Description:</label>
                <textArea class="form-control" name="description" onChange={this.onChange} placeholder="Description" cols="80" rows="3">{description}</textArea>
              </div>
              <div class="form-group">
                <label for="published_date">Published Date:</label>
                <input type="number" class="form-control" name="published_year" value={published_year} onChange={this.onChange} placeholder="Published Year" />
              </div>
              <div class="form-group">
                <label for="publisher">Publisher:</label>
                <input type="text" class="form-control" name="publisher" value={publisher} onChange={this.onChange} placeholder="Publisher" />
              </div>
              <button type="submit" class="btn btn-default">Submit</button>
            </form>
          </div>
        </div>
      </div>
    );
  }
}

export default Create;


MERN Stack: Add Show Component for Show Book Details

To show book details that listed on the home page, open and edit `src/components/Show.js` then add this lines of codes.

import React, { Component } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';

class Show extends Component {

  constructor(props) {
    super(props);
    this.state = {
      book: {}
    };
  }

  componentDidMount() {
    axios.get('/api/book/'+this.props.match.params.id)
      .then(res => {
        this.setState({ book: res.data });
        console.log(this.state.book);
      });
  }

  delete(id){
    console.log(id);
    axios.delete('/api/book/'+id)
      .then((result) => {
        this.props.history.push("/")
      });
  }

  render() {
    return (
      <div class="container">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title">
              {this.state.book.title}
            </h3>
          </div>
          <div class="panel-body">
            <h4><Link to="/"><span class="glyphicon glyphicon-th-list" aria-hidden="true"></span> Book List</Link></h4>
            <dl>
              <dt>ISBN:</dt>
              <dd>{this.state.book.isbn}</dd>
              <dt>Author:</dt>
              <dd>{this.state.book.author}</dd>
              <dt>Description:</dt>
              <dd>{this.state.book.description}</dd>
              <dt>Publish Date:</dt>
              <dd>{this.state.book.published_year}</dd>
              <dt>Publisher:</dt>
              <dd>{this.state.book.publisher}</dd>
            </dl>
            <Link to={`/edit/${this.state.book._id}`} class="btn btn-success">Edit</Link>&nbsp;
            <button onClick={this.delete.bind(this, this.state.book._id)} class="btn btn-danger">Delete</button>
          </div>
        </div>
      </div>
    );
  }
}

export default Show;

In this component, there is two button for edit current book and for delete current book. Delete function included in this component.


MERN Stack: Add Edit Component for Edit a Book

After the show book details, we need to edit the book. For that open and edit, `src/components/Edit.js` then add this lines of codes.

import React, { Component } from 'react';
import axios from 'axios';
import { Link } from 'react-router-dom';

class Edit extends Component {

  constructor(props) {
    super(props);
    this.state = {
      book: {}
    };
  }

  componentDidMount() {
    axios.get('/api/book/'+this.props.match.params.id)
      .then(res => {
        this.setState({ book: res.data });
        console.log(this.state.book);
      });
  }

  onChange = (e) => {
    const state = this.state.book
    state[e.target.name] = e.target.value;
    this.setState({book:state});
  }

  onSubmit = (e) => {
    e.preventDefault();

    const { isbn, title, author, description, published_year, publisher } = this.state.book;

    axios.put('/api/book/'+this.props.match.params.id, { isbn, title, author, description, published_year, publisher })
      .then((result) => {
        this.props.history.push("/show/"+this.props.match.params.id)
      });
  }

  render() {
    return (
      <div class="container">
        <div class="panel panel-default">
          <div class="panel-heading">
            <h3 class="panel-title">
              EDIT BOOK
            </h3>
          </div>
          <div class="panel-body">
            <h4><Link to={`/show/${this.state.book._id}`}><span class="glyphicon glyphicon-eye-open" aria-hidden="true"></span> Book List</Link></h4>
            <form onSubmit={this.onSubmit}>
              <div class="form-group">
                <label for="isbn">ISBN:</label>
                <input type="text" class="form-control" name="isbn" value={this.state.book.isbn} onChange={this.onChange} placeholder="ISBN" />
              </div>
              <div class="form-group">
                <label for="title">Title:</label>
                <input type="text" class="form-control" name="title" value={this.state.book.title} onChange={this.onChange} placeholder="Title" />
              </div>
              <div class="form-group">
                <label for="author">Author:</label>
                <input type="text" class="form-control" name="author" value={this.state.book.author} onChange={this.onChange} placeholder="Author" />
              </div>
              <div class="form-group">
                <label for="description">Description:</label>
                <input type="text" class="form-control" name="description" value={this.state.book.description} onChange={this.onChange} placeholder="Description" />
              </div>
              <div class="form-group">
                <label for="published_date">Published Date:</label>
                <input type="number" class="form-control" name="published_year" value={this.state.book.published_year} onChange={this.onChange} placeholder="Published Year" />
              </div>
              <div class="form-group">
                <label for="publisher">Publisher:</label>
                <input type="text" class="form-control" name="publisher" value={this.state.book.publisher} onChange={this.onChange} placeholder="Publisher" />
              </div>
              <button type="submit" class="btn btn-default">Submit</button>
            </form>
          </div>
        </div>
      </div>
    );
  }
}

export default Edit;


MERN Stack: Run the MERN App

Now, we can test our completed app by type this command.

npm run build
npm start

On the browser go to `localhost:3000` then you will see this home page.

Building CRUD Web Application using MERN Stack - MERN Application Home

You can navigate and populate the books data.


MERN Stack: Conclusion

This tutorial aims to make MERN stack as production MERN application, we run Express as the server and React build as the front end. For development purpose, you should separate Express server and React application then make Express CORS enable. Don't worry, you can get the full working source code on our GitHub.

That just the basic. If you need more deep learning about MERN Stack, React.js or React Native you can take the following cheap course:

Thanks!