30
MERN stack TODO application [Backend]
We'll be creating a minimal full-stack app using the
MERN
stack (MongoDB
for database, Express
and Node
for backend, and React
for frontend) to perform CRUD
operations.This series should enable you understand
CRUD
operations using MERN
stack.npm
and install necessary packagesNode
and Express
Todo
create
, read
, update
and delete
documents from the databaseAPI
routes using Insomnia
One should have at least some basic understanding of fundamental programming concepts and some experience with
HTML
, CSS
, JavaScript
.This post is not meant to explain the
MERN
stack, but it is a good introduction to building a full-stack app with it.VS Code
or any other editorNode.js
Insomnia
or PostmanPrettier
VS code extension to format the codeCreate a new folder and name it anything that you like and then open the folder in VS code and run the following code from the command prompt.
npm init -y
After running this command you will find a
package.json
if the folder.Run the following commands in the terminal to install the dependencies
npm i cors dotenv express mongoose
cors
: allows cross-origin api callsdotenv
: needed to access data from .env
filesexpress
: web application framework for node.jsmongoose
: It is needed to define the database schema and connecting to mongoDB
Now install the following development dependencies,
-D
is used to install the development dependencies.npm i -D nodemon
After installing the dependencies the
package.json
folder should look as follows.// package.json
{
"name": "mern-todo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"config": "^3.3.6",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"mongoose": "^5.13.2"
},
"devDependencies": {
"nodemon": "^2.0.11"
}
}
Now, create a
server.js
file and a .env
. The server.js
file will be the entry point of the server and the .env
file will contain the MONGO_URI
. We also have to make the following changes in the package.json
//package.json
{
"name": "mern-todo",
"version": "1.0.0",
"description": "",
"main": "server.js", //changed
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"config": "^3.3.6",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"mongoose": "^5.13.2"
},
"devDependencies": {
"nodemon": "^2.0.11"
}
}
Now, create the following folders
config
: Inside the config
folder, create a file named db.js
. This file will contain the required code for connecting to the MongoDB
database.
controllers
: The controllers
folder will contain the files which will have the methods for the end points to communicate with the database.
models
: The models
folder, will contain the files which will define the MongoDB schema
routers
: The routers
folder will contain the files with the endpoints
.
At this stage the file structure should look as follows
.
├── config
│ └── db.js
├── controllers
│ └── todo.js
├── models
│ └── todo.js
├── node_modules
├── routes
│ └── todo.js
├── .env
├── server.js
├── package-lock.json
└── package.json
"scripts": {
"start":"node server.js",
"dev":"nodemon server.js"
}
The
package.json
file should look as follows{
"name": "mern-todo",
"version": "1.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js", //added
"dev": "nodemon server.js" //added
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"config": "^3.3.6",
"cors": "^2.8.5",
"dotenv": "^10.0.0",
"express": "^4.17.1",
"mongoose": "^5.13.2"
},
"devDependencies": {
"nodemon": "^2.0.11"
}
}
We will do the following to setup the server
express
express()
get
method for the endpoint http://localhost:8000
using app.get()
PORT
to 8000
for our server to runPORT
using app.listen()
.
├── config
│ └── db.js
├── controllers
│ └── todo.js
├── models
│ └── todo.js
├── node_modules
├── routes
│ └── todo.js
├── .env
├── server.js <-- we are here
├── package-lock.json
└── package.json
The code will look as follows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server.js | |
const express = require("express"); | |
const app = express(); | |
// initialize middleware | |
app.use(express.json({ extended: false })); | |
app.get("/", (req, res) => res.send("Server up and running")); | |
// setting up port | |
const PORT = process.env.PORT || 8000; | |
app.listen(PORT, () => { | |
console.log(`server is running on http://localhost:${PORT}`); | |
}); |
And start the server using
nodemon
using the following code. Make sure you are running the following command from the project directory.npm run dev
If the server has started successfully then it should show the following message in the terminal
[nodemon] 2.0.11
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node server.js`
server is running on http://localhost:8000
You can also open
http://localhost:8000
on your browser.To connect to the database we will need the link for the
mongoDB
collection.allow access from anywhere
. Then Add IP address

//.env
MONGO_URI = mongodb+srv://<username>:<password>@cluster0.owmij.mongodb.net
Replace the
<username>
and <password>
with your database username and password which you will set in step 9..
├── config
│ └── db.js <-- we are here
├── controllers
│ └── todo.js
├── models
│ └── todo.js
├── node_modules
├── routes
│ └── todo.js
├── .env
├── server.js
├── package-lock.json
└── package.json
Now, open the
db.js
file which is in the config
folder and add the following changes.mongoose
MONGO_URI
from .env
connectDB
methof for connecting to the databaseconnectDB
method to be called in server.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// config/db.js | |
const mongoose = require("mongoose"); | |
const db = process.env.MONGO_URI; | |
const connectDB = async () => { | |
try { | |
await mongoose.connect(db, { | |
useNewUrlParser: true, | |
useUnifiedTopology: true, | |
}); | |
console.log("MongoDB is connected"); | |
} catch (err) { | |
console.error(err.message); | |
process.exit(1); | |
} | |
}; | |
module.exports = connectDB; |
Add the following changes in the
server.js
file.dotenv
connectDB
method from config/db.js
connectDB
method.Let us make the the following changes in
server.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server.js | |
require("dotenv").config(); //added | |
const express = require("express"); | |
const connectDB = require("./config/db"); //added | |
const app = express(); | |
// connect database | |
connectDB();//added | |
// initialize middleware | |
app.use(express.json({ extended: false })); | |
app.get("/", (req, res) => res.send("Server up and running")); | |
// setting up port | |
const PORT = process.env.PORT || 8000; | |
app.listen(PORT, () => { | |
console.log(`server is running on http://localhost:${PORT}`); | |
}); |
Save the changes it will restart the server or use the command
npm run dev
. The terminal should show a message of MongoDB is connected
which we have added in the db.js
under the try block.Create a
todo.js
file in the models folder. We will define the database schema in this file..
├── config
│ └── db.js
├── controllers
│ └── todo.js
├── models
│ └── todo.js <-- we are here
├── node_modules
├── routes
│ └── todo.js
├── .env
├── server.js
├── package-lock.json
└── package.json
mongoose
Schema
called TodoSchema
title
and description
title
will be String
and it is a mandatory fielddescription
will be String
and it is not a mandatory fieldThe code will look as follows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// models/todo.js | |
const mongoose = require("mongoose"); | |
const TodoSchema = new mongoose.Schema({ | |
title: { | |
type: "String", | |
required: true, | |
}, | |
description: { | |
type: "String", | |
}, | |
}); | |
const Todo = mongoose.model("todo", TodoSchema); | |
module.exports = Todo; |
.
├── config
│ └── db.js
├── controllers
│ └── todo.js
├── models
│ └── todo.js
├── node_modules
├── routes
│ └── todo.js <-- we are here
├── .env
├── server.js
├── package-lock.json
└── package.json
We will define the end points for the
CRUD
operationsexpress
router
controllers
GET
method to read
all the todoPOST
method to create
a new todoPUT
method to update
a existing todoDELETE
method to delete
a existing todorouter
The code will look as follows
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// router/todo.js | |
const express = require("express"); | |
const router = express.Router(); | |
/** | |
* @route GET api/todo | |
* @description get all todo | |
* @access public | |
*/ | |
router.get("/"); | |
/** | |
* @route POST api/todo | |
* @description add a new todo | |
* @access public | |
*/ | |
router.post("/"); | |
/** | |
* @route PUT api/todo/:id | |
* @description update todo | |
* @access public | |
*/ | |
router.put("/:id"); | |
/** | |
* @route DELETE api/todo/:id | |
* @description delete todo | |
* @access public | |
*/ | |
router.delete("/:id"); | |
module.exports = router; |
We will define the methods for the end points in the
controllers
folder.
├── config
│ └── db.js
├── controllers
│ └── todo.js <-- we are here
├── models
│ └── todo.js
├── node_modules
├── routes
│ └── todo.js
├── .env
├── server.js
├── package-lock.json
└── package.json
Todo
model from models/todo
getAllTodo
postCreateTodo
putUpdateTodo
deleteTodo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// controllers/todo.js | |
const Todo = require("../models/todo"); | |
exports.getAllTodo = (req, res) => { | |
Todo.find() | |
.then((todo) => res.json(todo)) | |
.catch((err) => | |
res | |
.status(404) | |
.json({ message: "Todo not found", error: err.message }) | |
); | |
}; | |
exports.postCreateTodo = (req, res) => { | |
Todo.create(req.body) | |
.then((data) => res.json({ message: "Todo added successfully", data })) | |
.catch((err) => | |
res | |
.status(400) | |
.json({ message: "Failed to add todo", error: err.message }) | |
); | |
}; | |
exports.putUpdateTodo = (req, res) => { | |
Todo.findByIdAndUpdate(req.params.id, req.body) | |
.then((data) => res.json({ message: "updated successfully", data })) | |
.catch((err) => | |
res | |
.status(400) | |
.json({ message: "Failed to update todo", error: err.message }) | |
); | |
}; | |
exports.deleteTodo = (req, res) => { | |
Todo.findByIdAndRemove(req.params.id, req.body) | |
.then((data) => | |
res.json({ message: "todo deleted successfully", data }) | |
) | |
.catch((err) => | |
res | |
.status(404) | |
.json({ message: "book not found", error: err.message }) | |
); | |
}; |
getAllTodo
: The find()
method will return all the todo in the collection. If the collection is empty then it will return a 404
error.postCreateTodo
: The create()
method will create a todo and return a success message. Otherwise, it will return a 400
error.putUpdateTodo
: The findByIdAndUpdate()
will require two parameters the id
and data
of the todo to be updated. The id
parameter will be extracted from req.params.id
.deleteTodo
: The findByIdAndRemove()
method will require only one parameter that is the id
of the todo..
├── config
│ └── db.js
├── controllers
│ └── todo.js
├── models
│ └── todo.js
├── node_modules
├── routes
│ └── todo.js <-- we are here
├── .env
├── server.js
├── package-lock.json
└── package.json
CRUD
operations
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// routes/todo.js | |
const express = require("express"); | |
const router = express.Router(); | |
const { | |
getAllTodo, | |
postCreateTodo, | |
putUpdateTodo, | |
deleteTodo, | |
} = require("../controllers/todo"); | |
/** | |
* @route GET api/todo | |
* @description get all todo | |
* @access public | |
*/ | |
router.get("/", getAllTodo); | |
/** | |
* @route POST api/todo | |
* @description add a new todo | |
* @access public | |
*/ | |
router.post("/", postCreateTodo); | |
/** | |
* @route PUT api/todo/:id | |
* @description update todo | |
* @access public | |
*/ | |
router.put("/:id", putUpdateTodo); | |
/** | |
* @route DELETE api/todo/:id | |
* @description delete todo | |
* @access public | |
*/ | |
router.delete("/:id", deleteTodo); | |
module.exports = router; |
.
├── config
│ └── db.js
├── controllers
│ └── todo.js
├── models
│ └── todo.js
├── node_modules
├── routes
│ └── todo.js
├── .env
├── server.js <-- we are here
├── package-lock.json
└── package.json
The final part of completing the backend is to add the endpoints to the
server.js
file.routes/todo.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// server.js | |
require("dotenv").config(); | |
const express = require("express"); | |
const connectDB = require("./config/db"); | |
const app = express(); | |
// routes | |
const todo = require("./routes/todo"); // added | |
// connect database | |
connectDB(); | |
// initialize middleware | |
app.use(express.json({ extended: false })); | |
app.get("/", (req, res) => res.send("Server up and running")); | |
// use routes | |
app.use("/api/todo", todo); // added | |
// setting up port | |
const PORT = process.env.PORT || 8000; | |
app.listen(PORT, () => { | |
console.log(`server is running on http://localhost:${PORT}`); | |
}); |
We will send a
POST
request to http://localhost:8000/api/todo

We will send a
GET
request to http://localhost:8000/api/todo

You can check the changes in mongoDB in
collections
To update a todo we will send a
PUT
request to http://localhost:8000/api/todo/id
The
id
has to be taken from the response message of the server.{
"message": "Todo added successfully",
"data": {
"_id": "60ec0f9655f9735a60a2d967",
"title": "test todo",
"description": "test todo",
"__v": 0
}
}
For updating the todo we will need the
id
. We will get the id
from the _id
from the preview tab. We can get the id
from the preview
after using the GET
request and POST
request.
To delete a todo we will send a
DELETE
request to http://localhost:8000/api/todo/id

.
├── config
│ └── db.js
├── controllers
│ └── todo.js
├── models
│ └── todo.js
├── node_modules
├── routes
│ └── todo.js
├── .env
├── server.js <-- we are here
├── package-lock.json
└── package.json
Added
cors
so that we can make the api calls from the frontend application like react.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
require("dotenv").config(); | |
const express = require("express"); | |
const cors = require("cors"); // added | |
const connectDB = require("./config/db"); | |
const app = express(); | |
// routes | |
const todo = require("./routes/todo"); | |
// connect database | |
connectDB(); | |
// cors | |
app.use(cors({ origin: true, credentials: true })); // added | |
// initialize middleware | |
app.use(express.json({ extended: false })); | |
app.get("/", (req, res) => res.send("Server up and running")); | |
// use routes | |
app.use("/api/todo", todo); | |
// setting up port | |
const PORT = process.env.PORT || 8000; | |
app.listen(PORT, () => { | |
console.log(`server is running on http://localhost:${PORT}`); | |
}); |
You can see the entire code of this blog in GitHub
30