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": "ozenero.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 }) => (
    <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>ozenero</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 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-sequelize-mysql---add-book

Show Books:

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---show-book

Check MySQL database:

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---mysql-book-adding-records

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

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---edit-book

Click Add Book button and check new Book list:

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---edit-book-return

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

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---result-remove-book

Check MySQL Database:

react-redux-http-client-nodejs-restapi-express-sequelize-mysql---result-show-books-database-after-edit

-> Sequelize’s Logs:


$npm start

> nodejs-react-restapi@1.0.0 start D:\gkz\article\Nodejs-React-RestAPI
> node server.js

App listening at http://:::8080
Executing (default): DROP TABLE IF EXISTS `books`;
Executing (default): DROP TABLE IF EXISTS `books`;
Executing (default): CREATE TABLE IF NOT EXISTS `books` (`id` INTEGER NOT NULL auto_increment , `title` VARCHAR(255), `author` VARCHAR(255), `description` VARCHAR(255), `published` INTEGER, `createdAt` DATETIME NOT NULL, `updatedAt` DATETIME NOT NULL, PRIMARY KEY (`id`)) ENGINE=InnoDB;
Executing (default): SHOW INDEX FROM `books`
Drop and Resync with { force: true }
Executing (default): SELECT `id`, `title`, `author`, `description`, `published`, `createdAt`, `updatedAt` FROM `books` AS `book`;
Executing (default): INSERT INTO `books` (`id`,`title`,`author`,`description`,`published`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'Origin','Dan Brown','Origin thrusts Robert Langdon into the dangerous intersection of humankind\'s two most enduring questions.',2017,'2018-12-30 08:13:16','2018-12-30 08:13:16');
Executing (default): INSERT INTO `books` (`id`,`title`,`author`,`description`,`published`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'Harry Potter and the Deathly Hallows','J. K. Rowling','The seventh and final novel of the Harry Potter series',2017,'2018-12-30 08:13:48','2018-12-30 08:13:48');
Executing (default): INSERT INTO `books` (`id`,`title`,`author`,`description`,`published`,`createdAt`,`updatedAt`) VALUES (DEFAULT,'The 100-Year-Old Man Who Climbed Out the Window and Disappeared','Jonas Jonasson','',2009,'2018-12-30 08:14:06','2018-12-30 08:14:06');
Executing (default): UPDATE `books` SET `title`='Harry Potter and the Deathly Hallows',`author`='J. K. Rowling',`description`='The 7th and final novel of the Harry Potter series',`published`=2017,`updatedAt`='2018-12-30 08:22:35' WHERE `id` = '2'
Executing (default): DELETE FROM `books` WHERE `id` = '1'

Source Code

Nodejs-React-RestAPIs
ReactReduxHttpClient

7 thoughts on “How to Integrate React Redux + Nodejs/Express RestAPIs + Sequelize ORM – MySQL CRUD example”

  1. so I’m trying your example, very good by the way, but for some reason I’m getting an error when compiling and running the react-redux client. I’m getting a fail on the in app.js. I cloned your project did an npm init and an npm install, compared the code to this site and haven’t noticed any differences. Any ideas?

    1. so I’ve solved the first issue by adding a .babelrc file with “react” and “es2015” as presets. Now I’m getting errors in the books.js reducer file as the compiler doesn’t like the spread indicator (…) attached to state, book and action objects.

      1. Success!
        – Added the .babelrc file containing: {“presets” : [“react”, “es2015”]}
        – Removed all spread operators (…) in the books.js reducer file.
        – Removed .book from the end of action in the EDIT_BOOK reducer

  2. So here I am again. I need to modify (substituting budgets for books) [Budgets.]findAll() function/SQL statement to use an order by clause. Where would this be done? I tried expanding out the code from axios.get(‘budgets’).then… to axios.get(‘budgets order by catitem, subcatitem’).then… in the client actions.js file but that doesn’t seem to work. Where else would I make the change for the order by clause?

  3. 129255 722663This really is a appropriate blog for would like to find out about this topic. You realize a lot its almost challenging to argue along (not that I personally would wantHaHa). You in fact put the latest spin with a topic thats been discussed for a long time. Fantastic stuff, just amazing! 401175

Leave a Reply

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