Skip to content

Full-Stack Book Review Application with React, Flask, and SQLAlchemy

Building a Full-Stack Book Review Application with React, Flask, and SQLAlchemy

When it comes to creating a full-stack web application, combining a modern frontend framework like React with a robust backend like Flask can be incredibly powerful. Today, we'll walk through a project that showcases the harmony of these two technologies while managing a dataset of books, authors, series, and user-generated reviews.

This application allows users to view, upload, and manage book-related data, post reviews, and explore books by their favorite authors or series. Here's how we built it, step by step, starting with...

Project Architecture

The project is divided into two main parts:

  • Frontend: A React application for user interaction.
  • Backend: A Flask API that handles data storage and management using SQLAlchemy.

We also used tools like Flask-CORS for cross-origin requests, dotenv for environment variable management, and a simple SQLite database for persistence.

Frontend Overview

React

The frontend uses React to manage the user interface and state.

Features:

  • View and search for books: Fetch a list of books or search by title.
  • Add new books: Input forms allow users to add books, authors, or series.
  • User authentication: Users can register, log in, and log out using token-based authentication.
  • Review management: Users can add reviews for books they love.

Highlights:

  • Dynamic Routing: React Router manages navigation across pages like Books, Authors, Series, and Reviews.
  • Formik for Forms: To simplify form handling, we used Formik, complete with validation and error handling.

Here's a sneak peek into the Books component, where we handle the listing and filtering of books:

import "./books.scss";
import {useState} from "react";
import Book from "../../components/Book/Book.jsx";
import {useLocation} from "react-router";
import {useBooks} from "../../context/BooksContext.jsx";

export default function Books() {
    const location = useLocation();
    const queryParams = new URLSearchParams(location.search);
    const key = queryParams.get('search') || "";

    const {books} = useBooks();

    const [search, setSearch] = useState(key);

    const searchFillCallback = (search) => {
        setSearch(search);
    }

    const displayBooks = books?.filter((book) => {
            const searchString = search.trim().toLowerCase();
            return book.title?.toLowerCase().includes(searchString) ||
                book.author?.name.toLowerCase().includes(searchString) ||
                book.series?.name.toLowerCase().includes(searchString);
        }
    );

    return (
        <>
            <div className="search-container">
                <h1>Search for Books</h1>
                <input className="search-input"
                       type="text"
                       placeholder="Search by Title, Author, or Series"
                       value={search}
                       onChange={(e) => setSearch(e.target.value)}
                />
                <button className="search-clear" onClick={() => setSearch("")}>
                    <svg className={`${search.length > 0 ? "" : "active"}`}
                         xmlns="http://www.w3.org/2000/svg"
                         height="24px" viewBox="0 -960 960 960" width="24px">
                        <path
                            d="M480-424 284-228q-11 11-28 11t-28-11q-11-11-11-28t11-28l196-196-196-196q-11-11-11-28t11-28q11-11 28-11t28 11l196 196 196-196q11-11 28-11t28 11q11 11 11 28t-11 28L536-480l196 196q11 11 11 28t-11 28q-11 11-28 11t-28-11L480-424Z"/>
                    </svg>
                </button>
            </div>
            {displayBooks && displayBooks.length > 0 ? (
                <ul className="book-list">
                    {displayBooks.map((book) => (
                        <Book key={book.id} books={books} book={book}
                              searchFillCallback={searchFillCallback}/>
                    ))}
                </ul>
            ) : (
                <p>No books found.</p>
            )}
        </>
    );
}

Backend Overview

The backend API, built with Flask, is the core of the application, managing all data operations.

Key Endpoints

Books Management

  • GET /books: Fetch all books.
  • POST /books: Add a new book, with optional association to an author and a series.
  • PATCH /books/<id>: Update a book's details.
  • DELETE /books/<id>: Delete a book.

User Authentication

  • POST /users: Register a new user.
  • POST /users/login: Log in and retrieve a secure access token.
  • DELETE /users/logout: Log out and invalidate the user's token.

Reviews

  • GET /books/<id>/reviews: Fetch reviews for a book.
  • POST /reviews: Add a new review (user authentication required).

Database Models

The database uses SQLAlchemy ORM to define relationships between entities like Book, Author, Series, User, and Review. Here's the Book model:

class Book(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    title = db.Column(db.String(80), unique=False, nullable=False)
    description = db.Column(db.String(500), unique=False, nullable=False)
    published = db.Column(db.Date, unique=False, nullable=False)

    author_id = db.Column(db.Integer, db.ForeignKey('author.id'))
    author = db.relationship('Author', backref='book')

    series_id = db.Column(db.Integer, db.ForeignKey('series.id'), nullable=True)
    series = db.relationship('Series', backref='book')

    def to_json(self):
        return {
            'id': self.id,
            'title': self.title,
            'description': self.description,
            'published': self.published,
            'author': self.author.to_json(),
            'series': self.series.to_json() if self.series else None
        }

Authentication

User authentication uses a custom token-based system with access tokens stored in the database. This ensures secure communication and user-specific operations.

The utils.py file includes helper functions like get_user_from_token, which simplifies token-based user verification:

def get_user_from_token():
    auth_header = request.headers.get('Authorization')
    if not auth_header:
        return None, 'Access token required!', 401

    token = auth_header.split(" ")[1] if len(
        auth_header.split(" ")) == 2 else None
    if not token:
        return None, 'Access token is malformed!', 401

    user = User.query.filter_by(access_token=token).first()
    if not user:
        return None, 'Invalid access token!', 401

    return user, None, None

Future Enhancements

  • Pagination: Add pagination to manage large datasets of books or reviews.
  • Advanced Authentication: Use OAuth2 or JWT for better security.
  • Search Filters: Enhance the search functionality with filters for authors, series, and publication dates.
  • UI Improvements: Add animations and better visual feedback.

This project is a great starting point for anyone looking to build a full-stack web application. It demonstrates the power of combining modern technologies like React and Flask with fundamental web concepts like RESTful APIs and relational databases. Try it out and expand it further to meet your unique needs!