MERN Stack Refresher

CRUD Operations for a Book Store

MERN Stack Refresher

This is a beginner-friendly project to understand the CRUD operations in Node.js and even if you are a professional developer but you have not worked on MERN stack applications for a long time, this project will serve as a refresher.

Backend

I assume you are using VS Code as an editor. Start by creating a folder and create a backend folder within it.

Initialize the project

We need to initialize the new project, hence we need a package.json file that contains metadata about your project and dependencies ie the third-party libraries you install. Hence, run this command in the very beginning in the terminal of the backend folder.

npm init -y

Here, -y stands for yes which is used to avoid the step-by-step process of initializing of project name, version, etc. This simply accepts the default values.

Installing express and nodemon

We need express and nodemon now. As mentioned already, the versions of these will be visible automatically in the dependencies section of package.json after installation. Because of this, if you want to share the project with anyone, they do not need to install every individual library/package but instead run npm i and it will install everything necessary for the project from package.json. Hence, package.json is a great help when trying to recreate the project.

npm i express nodemon

Now, nodemon is used to automatically start the server when changes are detected in the code. Express is a framework that is useful for creating REST APIs and middleware etc that we will see later. It makes managing servers and routes very easy.

Running the server

Lets now add a line to tell the server that we will use nodemon to run the server by adding this line in package.json to the scripts section.

"dev": "nodemon index.js"

Now, if we run the following in the terminal,

npm run dev

it will start nodemon directing to index.js which will restart the server automatically. So create an index.js file in the base folder before this.

Config.js

Create config.js file to define the port

export const PORT = 8000; // ES6 module syntax
// module.exports.PORT = 8000; for CommonJS module syntax

In the above code, there are two different module syntaxes. If you are using ES6 module syntax, then you can use import/export statements but if you are using CommonJS module syntax then you have to use require instead of import and module.exports instead of export. If you are getting an error for using export/import, then you need to add “type”: “module” line to your package.json below the description. So your package.json should look like this (versions might differ).

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "nodemon index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "mongoose": "^8.0.0",
    "nodemon": "^3.0.1"
  }
}

By default, it is CommonJS module syntax that uses require. Hence we need to configure ES6 syntax to use import/export. That is because, in Node.js environments up to version 13 and older, the default syntax is CommonJS. But in this blog, I am using ES6 module syntax, you are free to use any of your choice.

index.js

Let us start by importing express in index.js file. Refer to the comments in the code snippet below.

import express from 'express'; // ES6 module syntax
// const express = require('express'); for CommonJS module syntax
import { PORT } from './config.js'; // ES6 module syntax
// const { PORT } = require('./config'); for CommonJS module syntax

// create an instance of the express application
const app = express();

// listen is a function of Express to start a server and listen to 
// incoming connections with a callback function as second argument that 
// gets executed when server starts
app.listen(PORT, () => {
    console.log(`App is running on the port: ${PORT}`);
})

Connecting to the database: MongoDB

Go to this website and create an account if not already exist.

Cloud: MongoDB Cloud

Edit description

account.mongodb.com

Click on Create a database, and select the shared cluster because it is free and your cloud database is created. Once created, click on the connect option, and add your IP address. Select drivers and you will see a section

Add your connection string into your application code

Your MongoDB URL is present here, which will be used in the code to connect to the database. Copy that and paste it into the config.js file.

export const PORT = 8000; // ES6 module syntax
// module.exports.PORT = 8000; for CommonJS module syntax

export const mongoDBURL = 'YOUR MONGODB URL';
# mongodb://username:password@host:port/database
# replace the username and password according to your login credentials

We need to install Mongoose which is an Object Data Modeling Library for MongoDB which means it is useful in retrieving data, modifying it, creating it, etc in the MongoDB database. You will see this in the later part of the code.

npm i mongoose

In the index.js file, import the MongoDB URL as well.

import express from 'express'; // ES6 module syntax
// const express = require('express'); for CommonJS module syntax
import { PORT, mongoDBURL } from './config.js'; // ES6 module syntax
// const { PORT } = require('./config'); for CommonJS module syntax
import mongoose from 'mongoose';

// create an instance of the express application
const app = express();

// mongoose is a Object Data Modeling library for MongoDB in Node.js, 
// connect() uses Promise based approach
mongoose.connect(mongoDBURL).then(() => {
    console.log('Connected to Database');
 // listen is a function of Express to start a server and listen to 
// incoming connections with a callback function as second argument that gets 
// executed when server starts
    app.listen(PORT, () => {
        console.log(`App is running on the port: ${PORT}`);
    })
}).catch((error) => {
    console.log(error);
})

MVC Pattern

We will use the MVC pattern here, hence we create a models folder to define our models and create a file called bookModel.js.

Models in the MVC pattern define the structure, rules, and relationship of the data being managed by the application. Hence, we define our schema here. As we know for storing a book, mainly we need the book title, author name, and the published year. We will define whether it should be string, number, and required is true for all of them as none of them should be empty. In the file bookModel.js add this below code and finally export it to be used by other files.

import mongoose from "mongoose"

const bookSchema = mongoose.Schema(
    {
        title: {
            type: String,
            required: true,
        },
        author: {
            type: String,
            required: true,
        },
        publishedYear: {
            type: Number,
            required: true,
        }
    },
    {
        timestamps: true,
    }
)

export const Book = mongoose.model('Book', bookSchema);

Creating REST APIs

Express.js provides a router which is an object that is used to organize routes in a modular way. For example, if we want to create HTTP requests for users and HTTP requests for managing books, then we will have at least 4 routes for each like GET, POST, PUT, DELETE. This makes 8 routes in total and it is not a good way of programming to have all of them in index.js file. Hence we create a routes folder and create individual files for each, may be like user.js, books.js. We import them into our main file which is index.js.

So now, create a routes folder, and since we only have to manage book data here, create a file called booksRoute.js. We import the bookSchema from bookModel.js and create a router using const router = express.Router();

We will create 5 routes and export them and they are imported in index.js file.

import express from "express";
import { Book } from "../models/bookModel.js";

const router = express.Router();

// get method of Express sets up HTTP GET for the path '/'
// the arrow function in the second argument is a request handler that takes req and res as 2 parameters. req has information on incoming http request and res sends response back to client
router.get('/', async(request, response) => {
    try {
        const books = await Book.find({});
        return response.status(200).json({
            count: books.length,
            data: books
        });
    } catch (error) {
        response.status(500).send({ message: error.message });
    }
});

router.get('/:id', async(request, response) => {
    try {
        const { id } = request.params;
        const book = await Book.findById(id);
        return response.status(200).json(book);
    } catch (error) {
        response.status(500).send({ message: error.message });
    }
});

router.post('/', async(request, response) => {
    try {
        if (
            !request.body.title ||
            !request.body.author ||
            !request.body.publishedYear
        ) {
            return response.status(400).send({
                message: 'Send all fields'
            });
        }
        const newBook = {
            title: request.body.title,
            author: request.body.author,
            publishedYear: request.body.publishedYear,
        };

        const book = await Book.create(newBook);
        return response.status(201).send(book);
    } catch (error) {
        response.status(500).send({message: error.message});
    }
});

router.put('/:id', async(request, response) => {
    try {
        if (
            !request.body.title ||
            !request.body.author ||
            !request.body.publishedYear
        ) {
            return response.status(400).send({
                message: 'Send all fields'
            });
        }

        const { id } = request.params;

        const result = await Book.findByIdAndUpdate(id, request.body);

        if(!result) {
            return response.status(404).json({message: 'Book not found'});
        }

        return response.status(200).send({message: 'Book updated successfully'});
    } catch (error) {
        response.status(500).send({ message: error.message });
    }
});

router.delete('/:id', async(request, response) => {
    try {
        const { id } = request.params;
        const result = await Book.findByIdAndDelete(id);
        if(!result) {
            return response.status(404).json({message: 'Book not found'});
        }
        return response.status(200).send({message: 'Book deleted successfully'});
    } catch (error) {
        response.status(500).send({ message: error.message });
    }
});

export default router;

We will import the route in index.js file.

import express from 'express'; // ES6 module syntax
// const express = require('express'); for CommonJS module syntax
import { PORT, mongoDBURL } from './config.js'; // ES6 module syntax
// const { PORT } = require('./config'); for CommonJS module syntax
import mongoose from 'mongoose';
import booksRoute from './routes/booksRoute.js';

// create an instance of the express application
const app = express();

// middleware for parsing request body
app.use(express.json());

app.use('/books', booksRoute);

// mongoose is a Object Data Modeling library for MongoDB in Node.js, connect() uses Promise based approach
mongoose.connect(mongoDBURL).then(() => {
    console.log('Connected to Database');
    // listen is a function of Express to start a server and listen to incoming connections with a callback function as second argument that gets executed when server starts
    app.listen(PORT, () => {
        console.log(`App is running on the port: ${PORT}`);
    })
}).catch((error) => {
    console.log(error);
})

I will now explain the five routes.

GET:

GET request is to retrieve the data from the database based on the path you specify in the first argument router.get('/'), and in this case, it is /books. In the index.js file, if you observe we have this line, this is used to mount the router to our application using app.use(). The request /books will be redirected to the routes defined in the router. Hence, /books act as our base path.

app.use('/books', booksRoute);

In the second argument of the GET request, we are using the async function so that we can use await. We use a try-catch block to avoid any application crash in case of an error. In the try-catch, we have await which is used to stop the execution of the code till we retrieve all the books from the database by using the find function provided by Mongoose.

const books = await Book.find({});

If everything works fine, we return the response in the JSON format with status code 200, otherwise, it goes to the catch block. We have one more GET request to get the details of a particular book by passing its ID. router.get(‘/:id’), here it is a dynamic parameter allowing the retrieval of a specific book by its unique identifier.

We extract the id parameter from the request URL using request.params and we are using JavaScript destructuring here by using curly braces {}.

const { id } = request.params;

It allows you to extract specific properties of an object and assign them to variables with the same names as the properties. This is equivalent to

const id = request.params.id;

We use the findById of Mongoose to retrieve the book details.

POST:

Since we already know that the fields title, author, and published year should not be empty. In the POST API we will check it by using the if condition and return a response message: Send all fields with a status code 400 if any field is empty. Otherwise, we create a new object called newBook and we call the create function of Mongoose to create it in the database. We then send a 201 status code to indicate that it has been successfully created. But we are sending the data in JSON format and it is not by default. Hence, we need to parse it using Express middleware in the index.js file.

app.use(express.json());

Middleware functions have access to the request object, the response object, and the next function in the application’s request-response cycle. These functions can perform tasks, modify request and response objects, terminate the request-response cycle, or pass control to the next middleware in the stack.

PUT:

This is similar to POST but we have to pass the id so that we can edit that particular book. We use

findByIdAndUpdate(id, request.body):

  • id is the unique identifier used to find the specific document to be updated.

  • request.body contains the data that will update the document.

This will return a response and if this is null then we say 404 status code — book not found else return with 200 status code.

DELETE

This is very obvious if you have understood the above routes.

Handling CORS

Run this to install

npm i cors

CORS is a very common error we get while calling the backend API through the frontend. CORS stands for Cross-Origin Resource Sharing and web browsers use this to control interactions between different origins. It is always good to resolve this in the backend code. Hence we added this line in index.js

app.use(cors());

This by default is *, which allows every origin to access your backend API. You can also specify the origin and methods if you want only some origin to access it. The full index.js file looks like this

import express from 'express'; // ES6 module syntax
// const express = require('express'); for CommonJS module syntax
import { PORT, mongoDBURL } from './config.js'; // ES6 module syntax
// const { PORT } = require('./config'); for CommonJS module syntax
import mongoose from 'mongoose';
import cors from 'cors';
import booksRoute from './routes/booksRoute.js';

// create an instance of the express application
const app = express();

// middleware for parsing request body
app.use(express.json());

// allow all origins with default *
app.use(cors());

// OR - allow only 3000 port
// app.use(
//     cors({
//         origin: 'http://localhost:3000',
//         methods: ['GET', 'POST', 'PUT', 'DELETE'],
//         allowedHeaders: ['Content-Type']
//     })
// )

app.use('/books', booksRoute);

// mongoose is a Object Data Modeling library for MongoDB in Node.js, connect() uses Promise based approach
mongoose.connect(mongoDBURL).then(() => {
    console.log('Connected to Database');
    // listen is a function of Express to start a server and listen to incoming connections with a callback function as second argument that gets executed when server starts
    app.listen(PORT, () => {
        console.log(`App is running on the port: ${PORT}`);
    })
}).catch((error) => {
    console.log(error);
})

Postman

I recommend installing Postman to check your APIs. I have mentioned the port to run the server as 8000. Hence, the API with the base path books becomes, localhost:8000/books

Postman UI

Frontend

Run the below commands outside the backend folder ie in the main folder.

npm create vite@latest
npm i react-router-dom@6
npm i axios

I usually use Tailwind CSS CDN for small projects, hence add this in the head section of the index.html file.

<script src="https://cdn.tailwindcss.com"></script>

We need react-router-dom for routing and axios is used for making HTTP requests. Now we are all set for the frontend part. I will let you design as you wish hence in this blog, I will only explain the necessary functionalities that the frontend should have in order to call backend APIs.

Defining Routes

In App.jsx file, we define the routes. We first need to create a folder called pages and create jsx files called Home, CreateBook, UpdateBook, DeleteBook, and ShowBook.

import './App.css';
import { Routes, Route } from 'react-router-dom';
import Home from './pages/Home';
import CreateBook from './pages/CreateBook';
import UpdateBook from './pages/UpdateBook';
import DeleteBook from './pages/DeleteBook';
import ShowBook from './pages/ShowBook';

function App() {

  return (
    <Routes>
      <Route path='/' element={<Home />} />
      <Route path='/books/create' element={<CreateBook />} />
      <Route path='/books/details/:id' element={<ShowBook />} />
      <Route path='/books/edit/:id' element={<UpdateBook />} />
      <Route path='/books/delete/:id' element={<DeleteBook />} />
    </Routes>
  )
}

export default App

We define BrowserRouter in main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import { BrowserRouter } from 'react-router-dom' 

ReactDOM.createRoot(document.getElementById('root')).render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
)

Home.jsx

We will use useEffect to call the API once when the page loads, useState hook to initialize the books as false. Inside useEffect, we use axios for GET requests and if we receive the response, we will update books by setBooks with response.data.data. When the data is loaded, we will map the books with book and index.

useEffect(() => {
    axios.get('http://localhost:8000/books')
    .then((response) => {
      setBooks(response.data.data);
    })
    .catch((error) => {
      console.log(error);
    })
  }, []);

{books.map((book, index) => ())}

You can use book.title, book.author, book.publishedYear to fill the table and use icons to indicate edit, delete, and look for details of a particular book. This will be given to Link by react-router-dom. For example

<Link to={`/books/details/${book._id}`}>
  // icon for details
</Link>

<Link to={`/books/edit/${book._id}`}>
   // icon for edit                   
</Link>

<Link to={`/books/delete/${book._id}`}>
   // icon for delete               
</Link>

ShowBook.jsx

We use destructuring of JavaScript to get id from the URL,

const { id } = useParams();

useParams() is provided by react-router-dom. We will use axios again inside useEffect() to retrieve the details of a selected book.

useEffect(() => {
    axios.get(`http://localhost:8000/books/${id}`)
    .then((response) => {
        setBook(response.data);
    })
    .catch((error) => {
        console.log(error);
    })
  }, [])

The DeleteBook.jsx similar to ShowBook.jsx, but instead of calling delete API inside useEffect(), we should call it inside a function and call the function when the user clicks on a delete button.

CreateBook.jsx

UpdateBook.jsx is almost the same as CreateBook.jsx. We will have input boxes in both files for users to enter the title, author, and published year. Using useState hook we will initialize the states,

const [title, setTitle] = useState('');
const [author, setAuthor] = useState('');
const [publishedYear, setPublishedYear] = useState('');

We define a function to save the notes that will be called upon clicking the save button. We use useNavigate() function from react-router-dom to navigate to home once we save the book.

const navigate = useNavigate();
const handleSaveBook = () => {
    const data = {
      title, 
      author,
      publishedYear,
    };
    axios.post('http://localhost:8000/books', data)
    .then(() => {
      navigate('/');
    })
    .catch((error) => {
      setLoading(false);
      alert('No!')
    })
  }

In UpdateBook.jsx, we need to call along with the id.

This brings us to the end of this refresher.

Acknowledgement: freeCodeCamp.org