How to Integrate React Redux + Nodejs/Express RestAPIs + Mongoose ODM – MongoDB CRUD example

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---feature-image

How to Integrate React Redux + Nodejs/Express RestAPIs + Mongoose ODM – MongoDB CRUD example

In this tutorial, we will build React Redux Http Client & Nodejs/Express RestAPIs Server example that uses Mongoose ODM to interact with MongoDB database and React as a front-end technology to make request and receive response.

Related posts:
Crud RestAPIs with NodeJS/Express, MongoDB using Mongoose
How to connect React with Redux – react-redux example

Technologies

– Webpack 4.4.1
– React 16.3.0
– Redux 3.7.2
– React Redux 5.0.7
– axios 0.18.0

– Node.js/Express
– Mongoose ODM

– MongoDB

Overview

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---overview-1

1. Nodejs/Express Server

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---backend-architecture

2. React Redux Client

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---react-redux-client

For more details about:
– Redux: A simple practical Redux example
– Middleware: Middleware with Redux Thunk
– Connecting React with Redux: How to connect React with Redux – react-redux example

Practice

1. Node.js Backend

– Project structure:

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---nodejs-project-structure

Setting up Nodejs/Express project

Init package.json by cmd:

npm init

Install express, mongoose & cors:

$npm install express cors mongoose --save

-> now package.json file:


{
  "name": "node.js-react-restapis",
  "version": "1.0.0",
  "description": "Node.js RestAPIs access MongoDB by Mongoose",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "Nodejs",
    "RestAPIs",
    "Express",
    "MongoDB",
    "Mongoose"
  ],
  "author": "grokonez.com",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.16.4",
    "mongoose": "^5.4.2"
  }
}

Setting up Mongoose connection

./app/config/mongodb.config.js file:


module.exports = {
    url: 'mongodb://localhost:27017/nodejs-demo'
}

Create Mongoose model

./app/model/book.model.js file:


const mongoose = require('mongoose');
var Schema = mongoose.Schema;
 
var bookSchema = new Schema({
    title: String,
    author: String,
	description: String,
	published: String	
});

bookSchema.method('toClient', function() {
    var obj = this.toObject();

    //Rename fields
    obj.id = obj._id;
    delete obj._id;

    return obj;
});

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

Express RestAPIs

Route

-> Define Book’s routes in ./app/route/book.route.js file:


module.exports = function(app) {
 
    const books = require('../controller/book.controller.js');
 
    // Create a new Book
    app.post('/api/books/create', books.create);
 
    // Retrieve all Books
    app.get('/api/books', books.findAll);
 
    // Retrieve a single Book by Id
    app.get('/api/books/:bookId', books.findOne);
	 
    // Update a Book with Id
    app.put('/api/books/:bookId', books.update);
 
    // Delete a Book with Id
    app.delete('/api/books/:bookId', books.delete);
}

Controller

-> Implement Book’s controller in ./app/controller/book.controller.js file:


const Book = require('../model/book.model.js');

// POST a Book
exports.create = (req, res) => {
    // Create a Book
    const book = new Book({
		title: req.body.title,
		author: req.body.author,
		description: req.body.description,
		published: req.body.published
    });
 
    // Save a Book into MongoDB
    book.save()
    .then(book => {
        res.send(book.toClient());
    }).catch(err => {
        res.status(500).send({
            message: err.message
        });
    });
};
 
// FETCH all Books
exports.findAll = (req, res) => {
    Book.find()
    .then(books => {		
		let returnedBooks = [];
		
		for (let i = 0; i < books.length; i++) {
			returnedBooks.push(books[i].toClient());
		}
		
        res.send(returnedBooks);
    }).catch(err => {
        res.status(500).send({
            message: err.message
        });
    });
};
 
// FIND a Book
exports.findOne = (req, res) => {
    Book.findById(req.params.bookId)
    .then(book => {
        if(!book) {
            return res.status(404).send({
                message: "Book not found with id " + req.params.bookId
            });            
        }
        res.send(book.toClient());
    }).catch(err => {
        if(err.kind === 'ObjectId') {
            return res.status(404).send({
                message: "Book not found with id " + req.params.bookId
            });                
        }
        return res.status(500).send({
            message: "Error retrieving Book with id " + req.params.bookId
        });
    });
};
 
// UPDATE a Book
exports.update = (req, res) => {
    // Find Book and update it
    Book.findOneAndUpdate({ _id: req.params.bookId }, {
		title: req.body.title,
		author: req.body.author,
		description: req.body.description,
		published: req.body.published
    }, {new: true})
    .then(book => {
        if(!book) {
            return res.status(404).send({
                message: "Book not found with id " + req.params.bookId
            });
        }
        res.send(book.toClient());
    }).catch(err => {
        if(err.kind === 'ObjectId') {
            return res.status(404).send({
                message: "Book not found with id " + req.params.bookId
            });                
        }
        return res.status(500).send({
            message: "Error updating Book with id " + req.params.bookId
        });
    });
};
 
// DELETE a Book
exports.delete = (req, res) => {
    Book.findByIdAndRemove(req.params.bookId)
    .then(book => {
        if(!book) {
            return res.status(404).send({
                message: "Book not found with id " + req.params.bookId
            });
        }
        res.send({message: "Book deleted successfully!"});
    }).catch(err => {
        if(err.kind === 'ObjectId' || err.name === 'NotFound') {
            return res.status(404).send({
                message: "Book not found with id " + req.params.bookId
            });                
        }
        return res.status(500).send({
            message: "Could not delete Book with id " + req.params.bookId
        });
    });
};

Server.js

server.js file:


var express = require('express');
var app = express();
var bodyParser = require('body-parser');
app.use(bodyParser.json())
 
const cors = require('cors')
const corsOptions = {
  origin: 'http://localhost:8081',
  optionsSuccessStatus: 200
}
app.use(cors(corsOptions))
 
// Configuring the database
const dbConfig = require('./app/config/mongodb.config.js');
const mongoose = require('mongoose');
 
mongoose.Promise = global.Promise;
 
// Connecting to the database
mongoose.connect(dbConfig.url)
.then(() => {
    console.log("Successfully connected to MongoDB.");    
}).catch(err => {
    console.log('Could not connect to MongoDB.');
    process.exit();
});
 
require('./app/route/book.route.js')(app);
 
// Create a Server
var server = app.listen(8080, function () {
 
  var host = server.address().address
  var port = server.address().port
 
  console.log("App listening at http://%s:%s", host, port)
 
})

2. React Redux Client

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---react-client-project-structure

2.1 Dependency

-> package.json:


{
  "name": "react-redux-nodejs",
  "version": "1.0.0",
  "main": "index.js",
  "author": "Grokonez.com",
  "license": "MIT",
  "scripts": {
    "serve": "live-server public",
    "build": "webpack",
    "dev-server": "webpack-dev-server"
  },
  "dependencies": {
    "babel-cli": "6.24.1",
    "babel-core": "6.25.0",
    "babel-loader": "7.1.4",
    "babel-plugin-transform-object-rest-spread": "6.26.0",
    "babel-preset-env": "1.6.1",
    "babel-preset-react": "6.24.1",
    "css-loader": "0.28.11",
    "node-sass": "4.8.3",
    "react": "16.3.0",
    "react-dom": "16.3.0",
    "react-modal": "3.3.2",
    "react-redux": "5.0.7",
    "react-router-dom": "4.2.2",
    "redux": "3.7.2",
    "sass-loader": "6.0.7",
    "style-loader": "0.20.3",
    "webpack": "4.4.1",
    "webpack-cli": "2.0.13",
    "webpack-dev-server": "3.1.1",
    "redux-thunk": "2.2.0",
    "axios":"0.18.0"
  }
}

.babelrc :


{
    "presets": [
        "env",
        "react"
    ],
    "plugins": [
        "transform-object-rest-spread"
    ]
}

-> Run cmd: yarn install.

2.2 Configure base URL

axios/axios.js:


import axios from 'axios';
 
export default axios.create({
    baseURL: 'http://localhost:8080/api'
});

2.3 Redux Action

actions/books.js :


import axios from '../axios/axios';
 
const _addBook = (book) => ({
    type: 'ADD_BOOK',
    book
});
 
export const addBook = (bookData = {
    title: '',
    description: '',
    author: '',
    published: 0
}) => {
    return (dispatch) => {
        const book = {
            title: bookData.title,
            description: bookData.description,
            author: bookData.author,
            published: bookData.published
        };
 
        return axios.post('books/create', book).then(result => {
            dispatch(_addBook(result.data));
        });
    };
};
 
const _removeBook = ({ id } = {}) => ({
    type: 'REMOVE_BOOK',
    id
});
 
export const removeBook = ({ id } = {}) => {
    return (dispatch) => {
        return axios.delete(`books/${id}`).then(() => {
            dispatch(_removeBook({ id }));
        })
    }
};
 
const _editBook = (id, updates) => ({
    type: 'EDIT_BOOK',
    id,
    updates
});
 
export const editBook = (id, updates) => {
    return (dispatch) => {
        return axios.put(`books/${id}`, updates).then(() => {
            dispatch(_editBook(id, updates));
        });
    }
};
 
const _getBooks = (books) => ({
    type: 'GET_BOOKs',
    books
});
 
export const getBooks = () => {
    return (dispatch) => {
        return axios.get('books').then(result => {
            const books = [];
 
            result.data.forEach(item => {
                books.push(item);
            });
 
            dispatch(_getBooks(books));
        });
    };
};

2.4 Redux Reducer

reducers/books.js:


const booksReducerDefaultState = [];
 
export default (state = booksReducerDefaultState, action) => {
    switch (action.type) {
        case 'ADD_BOOK':
            return [
                ...state,
                action.book
            ];
        case 'REMOVE_BOOK':
            return state.filter(({ id }) => id !== action.id);
        case 'EDIT_BOOK':
            return state.map((book) => {
                if (book.id === action.id) {
                    return {
                        ...book,
                        ...action.updates
                    };
                } else {
                    return book;
                }
            });
        case 'GET_BOOKs':
            return action.books;
        default:
            return state;
    }
};

2.5 Redux Store

store/store.js:


import { createStore, applyMiddleware } from "redux";
import books from '../reducers/books';
import thunk from 'redux-thunk';
 
export default () => {
    return createStore(books, applyMiddleware(thunk));
};

2.6 React Components

components/Book.js:

import React from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { removeBook } from '../actions/books';
 
const Book = ({ id, title, description, author, published, dispatch }) => (
    <div>
        <Link to={`/book/${id}`}>
            <h4>{title} ({published})</h4>
        </Link>
        <p>Author: {author}</p>
        {description && <p>{description}</p>}
        <button onClick={() => {
            dispatch(removeBook({ id }));
        }}>Remove</button>
    </div>
);
 
export default connect()(Book);

components/DashBoard.js :

import React from 'react';
import BookList from './BookList';
 
const DashBoard = () => (
    <div className='container__list'>
        <BookList />
    </div>
);
 
export default DashBoard;

components/BookList.js:


import React from 'react';
import { connect } from 'react-redux';
import Book from './Book';
 
const BookList = (props) => (
    <div>
        Book List:
        <ul>
            {props.books.map(book => {
                return (
                    <li key={book.id}>
                        <Book {...book} />
                    </li>
                );
            })}
        </ul>
 
    </div>
);
 
const mapStateToProps = (state) => {
    return {
        books: state
    };
}
 
export default connect(mapStateToProps)(BookList);

components/AddBook.js:

import React from 'react';
import BookForm from './BookForm';
import { connect } from 'react-redux';
import { addBook } from '../actions/books';
 
const AddBook = (props) => (
    <div>
        <h3>Set Book information:</h3>
        <BookForm
            onSubmitBook={(book) => {
                props.dispatch(addBook(book));
                props.history.push('/');
            }}
        />
    </div>
);
 
export default connect()(AddBook);

components/EditBook.js:

import React from 'react';
import BookForm from './BookForm';
import { connect } from 'react-redux';
import { editBook } from '../actions/books';
 
const EditBook = (props) => (
    <div className='container__box'>
        <BookForm
            book={props.book}
            onSubmitBook={(book) => {
                props.dispatch(editBook(props.book.id, book));
                props.history.push('/');
            }}
        />
    </div>
);
 
const mapStateToProps = (state, props) => {
    return {
        book: state.find((book) =>
            book.id == props.match.params.id)
    };
};
 
export default connect(mapStateToProps)(EditBook);

components/BookForm.js:

import React from 'react';
 
export default class BookForm extends React.Component {
    constructor(props) {
        super(props);
        this.onTitleChange = this.onTitleChange.bind(this);
        this.onAuthorChange = this.onAuthorChange.bind(this);
        this.onDescriptionChange = this.onDescriptionChange.bind(this);
        this.onPublishedChange = this.onPublishedChange.bind(this);
        this.onSubmit = this.onSubmit.bind(this);
 
        this.state = {
            title: props.book ? props.book.title : '',
            author: props.book ? props.book.author : '',
            description: props.book ? props.book.description : '',
            published: props.book ? props.book.published : 0,
 
            error: ''
        };
    }
 
    onTitleChange(e) {
        const title = e.target.value;
        this.setState(() => ({ title: title }));
    }
 
    onAuthorChange(e) {
        const author = e.target.value;
        this.setState(() => ({ author: author }));
    }
 
    onDescriptionChange(e) {
        const description = e.target.value;
        this.setState(() => ({ description: description }));
    }
 
    onPublishedChange(e) {
        const published = parseInt(e.target.value);
        this.setState(() => ({ published: published }));
    }
 
    onSubmit(e) {
        e.preventDefault();
 
        if (!this.state.title || !this.state.author || !this.state.published) {
            this.setState(() => ({ error: 'Please set title & author & published!' }));
        } else {
            this.setState(() => ({ error: '' }));
            this.props.onSubmitBook(
                {
                    title: this.state.title,
                    author: this.state.author,
                    description: this.state.description,
                    published: this.state.published
                }
            );
        }
    }
 
    render() {
        return (
            <div>
                {this.state.error && <p className='error'>{this.state.error}</p>}
                <form onSubmit={this.onSubmit} className='add-book-form'>
 
                    <input type="text" placeholder="title" autoFocus
                        value={this.state.title}
                        onChange={this.onTitleChange} />
                    <br />
 
                    <input type="text" placeholder="author"
                        value={this.state.author}
                        onChange={this.onAuthorChange} />
                    <br />
 
                    <textarea placeholder="description"
                        value={this.state.description}
                        onChange={this.onDescriptionChange} />
                    <br />
 
                    <input type="number" placeholder="published"
                        value={this.state.published}
                        onChange={this.onPublishedChange} />
                    <br />
                    <button>Add Book</button>
                </form>
            </div>
        );
    }
}

2.7 React Router

routers/AppRouter.js:

import React from 'react';
import { BrowserRouter, Route, Switch } from 'react-router-dom';
import Header from '../components/Header';
import DashBoard from '../components/DashBoard';
import AddBook from '../components/AddBook';
import EditBook from '../components/EditBook';
import NotFound from '../components/NotFound';
 
const AppRouter = () => (
    <BrowserRouter>
        <div className='container'>
            <Header />
            <Switch>
                <Route path="/" component={DashBoard} exact={true} />
                <Route path="/add" component={AddBook} />
                <Route path="/book/:id" component={EditBook} />
                <Route component={NotFound} />
            </Switch>
        </div>
    </BrowserRouter>
);
 
export default AppRouter;

components/Header.js :

import React from 'react';
import { NavLink } from 'react-router-dom';
 
const Header = () => (
    <header>
        <h2>grokonez</h2>
        <h4>Book Mangement Application</h4>
        <div className='header__nav'>
            <NavLink to='/' activeClassName='activeNav' exact={true}>Dashboard</NavLink>
            <NavLink to='/add' activeClassName='activeNav'>Add Book</NavLink>
        </div>
    </header>
);
 
export default Header;

2.8 Render App

app.js :

import React from 'react';
import ReactDOM from 'react-dom';
import AppRouter from './routers/AppRouter';
import getAppStore from './store/store';
import { getBooks } from './actions/books';
import './styles/styles.scss';
 
import { Provider } from 'react-redux';
 
const store = getAppStore();
 
const template = (
    <Provider store={store}>
        <AppRouter />
    </Provider>
);
 
store.dispatch(getBooks()).then(() => {
    ReactDOM.render(template, document.getElementById('app'));
});

Run & Check Results

– Run MongoDB: mongod.exe
– Run Nodejs project with commandlines: npm start
– Run the React App with command: yarn run dev-server

– Open browser for url http://localhost:8081/:
Add Book:

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---add-book

Show Books:

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---show-book

Check MongoDB database:

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---mongodb-book-adding-records

Click on a Book’s title, app goes to Edit Page:

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---edit-book

Click Add Book button and check new Book list:

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---edit-book-return

Click on certain Remove button to remove certain Book.
For example, removing Origin:

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---result-remove-book

Check MongoDB Database:

react-redux-http-client-nodejs-restapi-express-mongoose-mongodb---mongodb-book-after-edit

Source Code

ReactReduxHttpClient
Nodejs-RestAPIs-MongoDB



By grokonez | January 6, 2019.

Last updated on March 29, 2021.



Related Posts


Got Something To Say:

Your email address will not be published. Required fields are marked *

*