How to Integrate React Redux + Nodejs/Express RestAPIs + Sequelize ORM – MySQL CRUD example

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---feature-image

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

Related posts:
Sequelize ORM – Build CRUD RestAPIs with NodeJs/Express, Sequelize, MySQL
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
– Sequelize

– MySQL 5.7.16

Overview

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---overview-1

1. Nodejs/Express Server

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---backend-architecture

2. React Redux Client

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---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-sequelize-mysql---nodejs-project-structure

Setting up Nodejs/Express project

Init package.json by cmd:

npm init

Install express, mysql, sequelize & cors:

$npm install express cors sequelize mysql2 --save

-> now package.json file:

{
  "name": "nodejs-react-restapi",
  "version": "1.0.0",
  "description": "Nodejs React RestAPIs",
  "main": "server.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [
    "Nodejs",
    "React",
    "RestAPI",
    "Redux",
    "MySQL"
  ],
  "author": "grokonez.com",
  "license": "ISC",
  "dependencies": {
    "cors": "^2.8.5",
    "express": "^4.16.4",
    "mysql2": "^1.6.4",
    "sequelize": "^4.42.0"
  }
}
Setting up Sequelize MySQL connection

– Create ./app/config/env.js file:

const env = {
  database: 'testdb',
  username: 'root',
  password: '12345',
  host: 'localhost',
  dialect: 'mysql',
  pool: {
	  max: 5,
	  min: 0,
	  acquire: 30000,
	  idle: 10000
  }
};
 
module.exports = env;

– Setup Sequelize-MySQL connection in ./app/config/db.config.js file:

const env = require('./env.js');
 
const Sequelize = require('sequelize');
const sequelize = new Sequelize(env.database, env.username, env.password, {
  host: env.host,
  dialect: env.dialect,
  operatorsAliases: false,
 
  pool: {
    max: env.max,
    min: env.pool.min,
    acquire: env.pool.acquire,
    idle: env.pool.idle
  }
});
 
const db = {};
 
db.Sequelize = Sequelize;
db.sequelize = sequelize;
 
//Models/tables
db.books = require('../model/book.model.js')(sequelize, Sequelize);
 
 
module.exports = db;
Create Sequelize model

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

module.exports = (sequelize, Sequelize) => {
	const Book = sequelize.define('book', {
	  title: {
		type: Sequelize.STRING
	  },
	  author: {
		type: Sequelize.STRING
	  },
	  description: {
		type: Sequelize.STRING
	  },
	  published: {
		  type: Sequelize.INTEGER
	  }
	});
	
	return Book;
}
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.findById);
	 
    // 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 db = require('../config/db.config.js');
const Books = db.books;
 
// Post a Book
exports.create = (req, res) => {	
	// Save Book to MySQL database
	Books.create({  
		title: req.body.title,
		author: req.body.author,
		description: req.body.description,
		published: req.body.published
	}).then(book => {
		// Send created book to client
		res.send(book);
	}).catch(err => {
		res.status(500).send("Error -> " + err);
	})
};
 
// Fetch all Books
exports.findAll = (req, res) => {
	Books.findAll().then(books => {
		// Send all Books to Client
		res.send(books);
	}).catch(err => {
		res.status(500).send("Error -> " + err);
	})
};
 
// Find a Customer by Id
exports.findById = (req, res) => {	
	Books.findById(req.params.bookId).then(book => {
		res.send(book);
	}).catch(err => {
		res.status(500).send("Error -> " + err);
	})
};
 
// Update a Book
exports.update = (req, res) => {
	var book = req.body;
	const id = req.params.bookId;
	Books.update({ 
					title: req.body.title,
					author: req.body.author,
					description: req.body.description,
					published: req.body.published
				}, 
				{ 
					where: {
						id: req.params.bookId
					} 
				})
				.then(() => {
						res.status(200).send(book);
				   }).catch(err => {
						res.status(500).send("Error -> " + err);
				   })
};
 
// Delete a Book by Id
exports.delete = (req, res) => {
	const id = req.params.bookId;
	Books.destroy({
				where: { id: id }
			}).then(() => {
				res.status(200).send('Book has been deleted!');
			}).catch(err => {
				res.status(500).send("Fail to delete!");
			});
};
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))
 
const db = require('./app/config/db.config.js');
  
// force: true will drop the table if it already exists
db.sequelize.sync({force: true}).then(() => {
  console.log('Drop and Resync with { force: true }');
});
 
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-sequelize-mysql---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 }) => (
    

{title} ({published})

Author: {author}

{description &&

{description}

}
); export default connect()(Book);

components/DashBoard.js :

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

components/BookList.js:

import React from 'react';
import { connect } from 'react-redux';
import Book from './Book';
 
const BookList = (props) => (
    
Book List:
    {props.books.map(book => { return (
  • ); })}
); 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) => (
    

Set Book information:

{ props.dispatch(addBook(book)); props.history.push('/'); }} />
); 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) => (
    
{ props.dispatch(editBook(props.book.id, book)); props.history.push('/'); }} />
); 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 (
            
{this.state.error &&

{this.state.error}

}


*