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
1. Nodejs/Express Server
2. 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:
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
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:
Show Books:
Check MySQL database:
Click on a Book’s title, app goes to Edit Page:
Click Add Book button and check new Book list:
Click on certain Remove button to remove certain Book.
For example, removing Origin:
Check MySQL Database:
-> 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