MERN Stack CRUD App Example

MERN stack is a popular technology stack that contains java-script libraries like Node, Express, React, and MongoDB to persist the data. MERN stack uses the Node and Express library to build the back-end APIs and the ReactJS for front-end development. In this article, we will create a simple CRUD app using the MERN stack by creating a simple To-Do app.

We will use NodeJS and Express to create REST APIs that connect to MongoDB.

Then we will create another ReactJS front-end application that invokes the REST APIs.

Table of Contents

Creating MERN stack back-end app with Node, Express & MongoDB

We will create REST APIs to read, create, update and delete the To-Do items.

Create a Node JS application

Create a directory with the name to-do-backend and navigate inside it.

Our back-end application uses the Express framework and mongoose library to connect to the MongoDB database.

Run the below command to initialize the node js project. The command execution will create a package.json file inside the directory.

npm init

Next, we will install the required node packages.

Install the required packages by running the below command.

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

We installed the below node packages:

  • nodemon: This is a developer tool helps to set up and actively reload the node application in our local environment. Using the nodemon Tool avoids manually restarting the server whenever we make any changes to our code.
  • express: Express framework provides good support for handling the HTTP requests.
  • mongoose: MongoDB client library to connect to MongoDB database.
  • body-parser: Helps in converting the HTTP response to JSON format.
  • cors: Adds CORS support for the application.

Also, modify the script field of the package.json file by adding the below entry. This query launches our node js application whenever we run the application with the npm start command.

"scripts": {
    "start": "nodemon index.js"
 }

Creating basic express app

Create a file with the name index.js in the root folder and add the below content.

const express = require('express')
const app = express()
app.get("/",(req, res) => {
    res.send("Hello World!!")
})
app.listen(3000)

This file is the starting point of our application. Here, we are importing the express library and creating an express application instance. and the application listens to the HTTP requests over the port 3000.

Now, we can run the application by executing the below command.

npm start

This will result in below output.

node express hello world app

Creating the Mongo schema model

Let’s define the MongoDB collection schema that will store our To-Do items.

Create a folder with the name models under the root directory and create a Todo.js file inside it. Add the below content.

const mongoose = require('mongoose')
const TodoSchema = mongoose.Schema({
    title:{
        type: String,
        required:true
    },
    description:{
        type: String
    },
    done:{
        type: Boolean,
        required: true
    }
})
module.exports = mongoose.model('todos', TodoSchema)

We have defined title and description fields that are of the type String. We also have a boolean field with the name done to indicate the completion of a To-Do task.

The schema uses the todos MongoDB collection.

Create the CRUD APIs

Let’s create the REST APIs using the express library.

Create a folder called routes and add todos.js inside it, as shown below.

const express = require('express')
const Todo = require('../models/Todo')
const router = express.Router()
router.get('/', (req, resp)=>{
    Todo.find().then(data => {
        resp.json(data)
    }).catch(e => {
        resp.json({message : e})
    })
})
router.post('/', (req, resp)=>{
    const todo = new Todo({
        title: req.body.title,
        description: req.body.description,
        done: false
    })
    todo.save().then(data => {
        resp.json(data)
    }).catch(e => {
        resp.json({message: e})
    })
})
router.patch('/:id', (req, resp) => {
    Todo.updateOne({_id: req.params.id}, {
            $set: {
                title: req.body.title, 
                description: req.body.description, 
                done: req.body.done
            }
        }).then(data => {
                resp.json(data)
        }).catch(e => {
                resp.json({message: e})
        })
})
router.delete('/:id', (req, resp)=>{
    Todo.deleteOne({_id: req.params.id})
    .then(data => {
        resp.json(data)
    }).catch(e => {
        resp.json({message: e})
    })
})
module.exports = router;

We are using the express Router to handle the incoming HTTP requests.

The POST API creates a new To-Do item and stores the data in MongoDB. The HTTP request body contains the title and description fields, and the done field value is set to false for a new To-Do item.

The PATCH and DELETE APIs expect the caller to pass the item id along with the HTTP request.

Update the index.js file

Update the index.js file with the below content.

const express = require('express')
const app = express()
const cors = require('cors')
const bodyParser = require('body-parser')
const todoRoute = require('./routes/todos')
const mongoose = require('mongoose')
app.use(cors())
app.use(bodyParser.json())
app.use('/todos', todoRoute)
mongoose.connect("mongo connection string").then(data => {
    console.log("connected to DB")
}).catch(error => {
    console.log(error)
})
app.listen(3000)

We are enabling CORS support and adding body-parser to the express application. The library also helps extract the JSON body from the incoming HTTP request.

Also, during the application startup, we are connecting to the back-end MongoDB database with the help of the mongoose library.

Testing the REST APIs

Let’s test the POST API. Run the application with the command npm start command.

We will create a To-Do item using the POST API.

node express POST api

To fetch the To-Do items use the GET API as shown below.

node express get api

Similarly, we can use the Delete API to delete the record by sending the id in the URL path.

node express delete api

For the To-Do item update, we will use the PATCH API.

node express patch api

Creating MERN stack front-end app with ReactJS

Now that our backend APIs are ready, let us create a React front-end application and consume those APIs.

Creating the react application

Create a new react application with the below command.

npx create-react-app

Navigate inside the created ReactJS application and install the bootstrap library.

npm i bootstrap

Also, inside the app/index.js file, add the below import.

import 'bootstrap/dist/css/bootstrap.min.css';

Now, update the App.js file with the below content.

import React from 'react'
function App() {
    return (
      <div className="App">
        Hello world!!
      </div>
    )
}
export default App;

We are replacing the default generated ReactJS code with a simple “Hello World” statement.

Run the application by executing the npm start command.

The ReactJS application starts in a random available port.

MERN stack app example

Adding the API calls

Let’s create a utility class to invoke the back-end APIs.

Create a file under a new folder called api under the src folder.

Add a new java-script file with the name todos.js and add the below content.

const API_URL = "http://localhost:3000/todos"
export async function getTodosAPI(){
    return fetch(API_URL)
    .then(resp => resp.json())
    .then(data => data)
    .catch(e => console.log(e))
}
export async function postTodosAPI(todo){
    return fetch(API_URL,{
        method: "POST",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(todo)
    })
    .then(resp => {
        return resp.json()
    })
    .then(data => data)
    .catch(e => console.log(e))
}
export async function patchTodosAPI(id, done){
    let todo = {
        _id: id,
        done: done
    }
    return fetch(API_URL+`/${id}`,{
        method: "PATCH",
        headers: {
            "Content-Type": "application/json"
        },
        body: JSON.stringify(todo)
    })
    .then(resp => resp.json())
    .then(data => data)
    .catch(e => console.log(e))
}
export async function deleteTodosAPI(id){
    return fetch(API_URL+`/${id}`,{
        method: "DELETE"
    })
    .then(resp => resp.json())
    .then(data => data)
    .catch(e => console.log(e))
}

We are using the JavaScript fetch library to invoke the To-Do REST APIs.

Adding To-Do item form

For creating a new To-Do item, we need to capture the title and description from the user. Let’s create a simple React component as shown below.

Create a file with the name CreateToDo.js under the src folder and add the below content.

import React, { useState, Fragment } from 'react'
function CreateToDo(props) {
    const { onCreate } = props
    
    const [book, setBook] = useState({
        title: "",
        description: "",
        done: false
    })
    const onChange = (e) => {
        setBook({
            ...book,
            [e.target.name]: e.target.value
        })
    }
    const saveTodo = (e) => {
        e.preventDefault()
        onCreate(book)
    }
    return (
        <Fragment>
                <form onSubmit={saveTodo}>
                    <h2 className="text-center m-3">To do APP</h2>
                    <div className="form-row d-flex justify-content-center">
                        <div className="col-3 m-1">
                            <input name = "title" type="text" 
                                className="form-control" placeholder="Title"
                                onChange={(e) => onChange(e)} />
                        </div>
                        <div className="col-5 d-flex justify-content-center m-1">
                            <input type="text" className="form-control" 
                                name = "description" placeholder="Description"
                                onChange={(e) => onChange(e)}/>
                        </div>
                        <button className='btn btn-primary col-2 d-flex justify-content-center m-1' 
                            type='submit'>Add</button>
                    </div>
                </form>
        </Fragment>
    )
}
export default CreateToDo

We are using the React State hook to maintain the user entered To-Do item details.

Once the user submits the form, we invoke the saveTodo function and send the value to the back-end.

Update the App.js file with the below content.

import React, { useState } from 'react';
import { postTodosAPI } from './api/todos'
import CreateToDo from './CreateToDo';
function App() {
  const [todos, setTodos] = useState([])
  const addTodo = (todo) => {
    postTodosAPI(todo).then(data => {
      setTodos([...todos, data])
    })
  }
    return (
      <main role="main" className="container">
        <CreateToDo onCreate={addTodo} />
      </main>  
    )
}
export default App;

Here, we are using the CreateToDo component that we created earlier. We are also passing the onCreate property that is assigned with the addTodo function.

Once the user submits the To-Do item form, that invokes this addTodo function and persists the record in the MongoDB.

The below images shows the add To-Do form. We can observe that once the user form submission invokes the POST API and we get the persisted record as the response.

mern stack app example

Displaying the To-Do items

To display the available To-Do items, let’s create a new component.

Create a new Component with the name TodoTable.js and add the below content.

import React from 'react'
function TodoTable(props) {
const { todos } = props
return (
<div className="App mt-5">
   <table className="table table-striped">
      <thead>
         <tr>
            <th scope="col">Title</th>
            <th scope="col">Description</th>
            <th scope="col">Done</th>
            <th scope="col">Delete</th>
         </tr>
      </thead>
      <tbody>
         {
         todos.map(todo => {
         return (
         <tr key={todo._id}>
            <td>{todo.title}</td>
            <td>{todo.description}</td>
            <td>
               {
               (todo.done) ? 
               (<img width="25" src="../../check.png" />) 
               :
               (<img width="25" src="../../uncheck.jpeg" />)
               }
            </td>
            <td>
               <button className='btn btn-danger'>Delete</button>
            </td>
         </tr>
         )
         })
         }
      </tbody>
   </table>
</div>
)
}
export default TodoTable

The component contains a simple table styled with Bootstrap CSS. Table displays title, description, an image to indicate the completion of the To-Do item, and a delete button.

Update the App.js by adding the highlighted part.

import React, { useState, useEffect } from 'react';
import { postTodosAPI, getTodosAPI } from './api/todos'
import CreateToDo from './CreateToDo';
import TodoTable from './TodoTable'
function App() {
  const [todos, setTodos] = useState([])
  useEffect(() => {
    getTodosAPI().then(todos => setTodos(todos))
  }, []);
  const addTodo = (todo) => {
    postTodosAPI(todo).then(data => {
      setTodos([...todos, data])
    })
  }
    return (
      <main role="main" className="container">
        <CreateToDo onCreate={addTodo} />
        <TodoTable todos={todos}/>
      </main>  
    )
}
export default App;

We are using the Effect hook of React to fetch the records from the back-end during the page rendering.

If we check the output, we can see the available To-Do items.

mern stack app example

Adding Update functionality

Let’s add an option to update the To-Do item. In this example, we will only allow the user to update the status of the To-Do item.

Update the TodoTable.js as shown below.

import React, { useState } from 'react'
function TodoTable(props) {
const { todos, onUpdate } = props
return (
<div className="App mt-5">
   <table className="table table-striped">
      <thead>
         <tr>
            <th scope="col">Title</th>
            <th scope="col">Description</th>
            <th scope="col">Done</th>
            <th scope="col">Delete</th>
         </tr>
      </thead>
      <tbody>
         {
         todos.map(todo => {
         return (
         <tr key={todo._id}>
            <td>{todo.title}</td>
            <td>{todo.description}</td>
            <td onClick={() => onUpdate(todo._id, todo.done)}>
               {
               (todo.done) ? 
               (<img width="25" src="../../check.png" />) 
               :
               (<img width="25" src="../../uncheck.jpeg" />)
               }
            </td>
            <td><button 
               className='btn btn-danger' >Delete</button></td>
         </tr>
         )
         })
         }
      </tbody>
   </table>
</div>
);
}
export default TodoTable

We added an onUpdate function, which invokes the back-end PATCH API whenever the user clicks on the check/un-check icon. We also pass the To-Do item id as the function parameter.

Update the App.JS file as shown below.

import React, { useState, useEffect, Fragment } from 'react';
import { getTodosAPI, postTodosAPI, patchTodosAPI } from './apis/todos'
import TodoTable from './TodoTable'
import CreateToDo from './CreateToDo';
function App() {
  const [todos, setTodos] = useState([])
  useEffect(() => {
    getTodosAPI().then(todos => setTodos(todos))
  }, []);
  const addTodo = (todo) => {
    postTodosAPI(todo).then(data => {
      console.log("data saved:" + JSON.stringify(data))
      setTodos([...todos, data])
    }
    )
  }
  const updateTodo = (id, done) => {
    patchTodosAPI(id, (done) ? false : true).then(data => {
      if(data){
        console.log('updating records!!')
        getTodosAPI().then(todos => setTodos(todos))
      }
    })
  }
  return (
    <Fragment>
      <main role="main" className="container">
        <CreateToDo onCreate={addTodo} />
        <TodoTable
          todos={todos}
          onUpdate={updateTodo} />
      </main>
    </Fragment>
  );
}
export default App;

The updateTodo function invokes the back-end API and updates the To-Do item’s status. We are also refreshing the item list by invoking the setTodos function.

Adding Delete functionality

Finally, let’s add delete functionality to our To-Do application.

Update the TodoTable.js file as shown below.

import React from 'react'
function TodoTable(props) {
const { todos, onUpdate, onDelete } = props
return (
<div className="App mt-5">
   <table className="table table-striped">
      <thead>
         <tr>
            <th scope="col">Title</th>
            <th scope="col">Description</th>
            <th scope="col">Done</th>
            <th scope="col">Delete</th>
         </tr>
      </thead>
      <tbody>
         {
         todos.map(todo => {
         return (
         <tr key={todo._id}>
            <td>{todo.title}</td>
            <td>{todo.description}</td>
            <td onClick={() => onUpdate(todo._id, todo.done)}>
               {
               (todo.done) ? 
               (<img width="25" src="../../check.png" />) 
               :
               (<img width="25" src="../../uncheck.jpeg" />)
               }
            </td>
            <td>
               <button 
                  className='btn btn-danger'
                  onClick={() => onDelete(todo._id)}>Delete</button>
            </td>
         </tr>
         )
         })
         }
      </tbody>
   </table>
</div>
)
}
export default TodoTable

The onDelete function deletes the To-Do item from MongoDB whenever the user clicks on the Delete button.

Also, update the App.js file by adding the deleteToDo function.

If the delete operation is successful, we also update the local To-Do list by removing the deleted item.

import React, { useState, useEffect } from 'react';
import { postTodosAPI, getTodosAPI, patchTodosAPI, deleteTodosAPI } from './api/todos'
import CreateToDo from './CreateToDo';
import TodoTable from './TodoTable'
function App() {
  const [todos, setTodos] = useState([])
  useEffect(() => {
    getTodosAPI().then(todos => setTodos(todos))
  }, []);
  const addTodo = (todo) => {
    postTodosAPI(todo).then(data => {
      setTodos([...todos, data])
    })
  }
  const updateTodo = (id, done) => {
    patchTodosAPI(id, (done) ? false : true).then(data => {
      if(data){
        getTodosAPI().then(todos => setTodos(todos))
      }
    })
  }
  const deleteTodo = (id) => {
    deleteTodosAPI(id).then(data => {
      if (data.deletedCount === 1) {
        setTodos(todos.filter(todo => todo._id !== id))
      }
    })
  }
    return (
      <main role="main" className="container">
        <CreateToDo onCreate={addTodo} />
        <TodoTable 
          todos={todos} 
          onUpdate={updateTodo}
          onDelete={deleteTodo}
        />
      </main>  
    )
}
export default App;

Testing the MERN stack app

Run the React JS application and make sure that the back-end application is up and available to receive the request at the location: http://localhost:3000.

Our To-Do application is now ready.

mern stack app example

Conclusion

In this article, we learned how to create a simple CRUD app using the MERN stack.

We learned how to create Node JS and Express-based REST API back-end apps. Then, we learned to use the mongoose node library to connect to the MongoDB database.

We also learned how to consume the APIs from a ReactJS application.

The application still has scope for improvements like input validation, allowing the user to update all the fields, pagination, etc.

I hope you have enjoyed the article. 🙂 Example code is available on Github.

Leave a Reply