In this tutorial, we will build React Redux Http Client & Nodejs/Express RestAPIs Server example that uses Sequelize ORM to interact with PostgreSQL database and React as a front-end technology to make request and receive response.
Related posts:
– Node.js/Express RestAPIs CRUD – Sequelize ORM – PostgreSQL
– How to connect React with Redux – react-redux example
Contents
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
– PostgreSQL
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
, postgresql
, sequelize
& cors
:
$npm install express cors sequelize pg pg-hstore --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", "PostgreSQL" ], "author": "grokonez.com", "license": "ISC", "dependencies": { "cors": "^2.8.5", "express": "^4.16.4", "pg": "^7.7.1", "pg-hstore": "^2.3.2", "sequelize": "^4.42.0" } }
Setting up Sequelize PostgreSQL connection
– Create ./app/config/env.js
file:
const env = { database: 'testdb', username: 'postgres', password: '123', host: 'localhost', dialect: 'postgres', pool: { max: 5, min: 0, acquire: 30000, idle: 10000 } }; module.exports = env;
– Setup Sequelize-PostgreSQL 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 PostgreSQL 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 }) => (); export default connect()(Book);{title} ({published})
Author: {author}
{description &&{description}
}
– 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:); const mapStateToProps = (state) => { return { books: state }; } export default connect(mapStateToProps)(BookList);{props.books.map(book => { return (
- ); })}
– components/AddBook.js
:
import React from 'react'; import BookForm from './BookForm'; import { connect } from 'react-redux'; import { addBook } from '../actions/books'; const AddBook = (props) => (); export default connect()(AddBook);Set Book information:
{ props.dispatch(addBook(book)); props.history.push('/'); }} />
– components/EditBook.js
:
import React from 'react'; import BookForm from './BookForm'; import { connect } from 'react-redux'; import { editBook } from '../actions/books'; const EditBook = (props) => (); const mapStateToProps = (state, props) => { return { book: state.find((book) => book.id == props.match.params.id) }; }; export default connect(mapStateToProps)(EditBook);{ props.dispatch(editBook(props.book.id, book)); props.history.push('/'); }} />
– 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}
}
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 = () => (); export default AppRouter;
– components/Header.js
:
import React from 'react'; import { NavLink } from 'react-router-dom'; const Header = () => (); export default Header; grokonez
Book Mangement Application
Dashboard Add Book
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 = (); 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 PostgreSQL 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 PostgreSQL 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" CASCADE; Executing (default): DROP TABLE IF EXISTS "books" CASCADE; Executing (default): CREATE TABLE IF NOT EXISTS "books" ("id" SERIAL , "title" VARCHAR(255), "author" VARCHAR(255), "description" VARCHAR(255), "published" INTEGER, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL, "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL, PRIMARY KEY ("id")); Executing (default): SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND t.relkind = 'r' and t.relname = 'books' GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname; 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,'2019-01-02 00:09:03.288 +00:00','2019-01-02 00:09:03.288 +00:00') RETURNING *; 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,'2019-01-02 00:09:27.491 +00:00','2019-01-02 00:09:27.491 +00:00') RETURNING *; 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,'2019-01-02 00:09:47.916 +00:00','2019-01-02 00:09:47.916 +00:00') RETURNING *; 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"='2019-01-02 00:35:35.040 +00:00' WHERE "id" = '2' Executing (default): DELETE FROM "books" WHERE "id" = '1'
Source Code
– ReactReduxHttpClient
– Node.js-React-RestAPI