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

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

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

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

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

1. Nodejs/Express Server

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

2. React Redux Client

react-redux-http-client-nodejs-restapi-express-sequelize-postgresql---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-postgresql---nodejs-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": "ozenero.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

react-redux-http-client-nodejs-restapi-express-sequelize-postgresql---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));
};
</pre>
<h5>2.6 React Components</h5>
- <code>components/Book.js</code>:

<pre>
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-postgresql---add-book

Show Books:

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

Check PostgreSQL database:

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

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

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

Click Add Book button and check new Book list:

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

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

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

Check PostgreSQL Database:

react-redux-http-client-nodejs-restapi-express-sequelize-postgresql---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" 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

One thought on “How to Integrate React Redux + Nodejs/Express RestAPIs + Sequelize ORM – PostgreSQL CRUD example”

Leave a Reply

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