Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

How to Build a Bookstore App with FARM Stack (FastAPI, React, and MongoDB)

by Jane Nkwor

.

Updated Fri May 16 2025

.
How to Build a Bookstore App with FARM Stack (FastAPI, React, and MongoDB)

The need to build full-stack applications that are fast, scalable and easy to maintain is now more important than ever. The FARM stack - FastAPI, React and MongoDB offers a combination of tools that can get the job done.

  • FastAPI is a high-performance web framework known for its speed and instant documentation used in building APIs with the Python programming language.

  • React is a popular JavaScript library used to build reliable, fast and scalable web applications. It allows for the creation of reusable UI components.

  • MongoDB is a flexible NoSQL database that makes data modelling and storage simple especially for JSON-like documents.

In this article, we’ll build a simple full-stack Bookstore application to show key CRUD operations using the FARM stack. The backend will showcase a RESTful API using FastAPI, the frontend will be built using the React library, and MongoDB will serve as the database. By the end of this article, you should be able to do the following:

  • View a list of books

  • Add new books

  • Edit an already existing book

  • Delete books

Prerequisites

  • Basic understanding of Python, JavaScript, and REST APIs

  • Tools required: Python 3.x, Node.js, MongoDB, npm/yarn,

  • Project structure overview

    • Create a directory for your app

mkdir bookstore_farm
cd bookstore_farm
  • Create subdirectories for the backend and frontend

mkdir backend frontend

Setting Up the Backend with FastAPI

Set up the Backend

  • Navigate to the backend directory, create a virtual environment and activate it

cd backend
python -m venv venv
# On Windows
.\venv\Scrips\activate
# On MacOS
source venv\bin\activate
  • In your terminal, install the necessary packages

pip install "fastapi[all]" "motor[srv]"
  • Generate the python packages in the requirements.txt file and install them

pip freeze > requirements.txt
pip install -r ./requirements.txt
  • Create three Python files

    • models.py: This file defines the data models used in the application. They are Pydantic schemas used to define the structure, and validate data.

    • main.py: This is the entry point for this application.

    • database.py: This file handles the MongoDB connection and abstracts the database operation like insert, find, update and delete.

Define Book model and Implement CRUD routes

from pydantic import BaseModel, Field
from typing import Optional
from bson import ObjectId

# Define the Book schema with the fields
class Book(BaseModel):
    id: str
    title: str
    author: str
    summary: str

    @staticmethod
    def from_doc(doc) -> "Book":
        return Book(
            id=str(doc["_id"]),
            title=doc["title"],
            author=doc["author"],
            summary=doc["summary"]
        )

class BookCreate(BaseModel):
    title: str
    author: str
    summary: str

class BookUpdate(BaseModel):
    title: Optional[str]
    author: Optional[str]
    summary: Optional[str]
from contextlib import asynccontextmanager
from datetime import datetime
import os
from dotenv import load_dotenv
import sys
from fastapi.middleware.cors import CORSMiddleware
from bson import ObjectId
from fastapi import FastAPI, Request, status, HTTPException, Depends, Request
from motor.motor_asyncio import AsyncIOMotorClient
from pydantic import BaseModel
import uvicorn

from models import Book, BookCreate, BookUpdate
from database import BookDAL

load_dotenv()

COLLECTION_NAME= "book_lists"
MONGODB_URI= os.environ["MONGODB_URI"]
DEBUG= os.environ.get("DEBUG", "").strip().lower() in {"1", "true", "on", "yes" }

@asynccontextmanager
async def lifespan(app: FastAPI):
    client = AsyncIOMotorClient(MONGODB_URI)
    database = client.get_default_database()

    pong = await database.command("ping")
    if int(pong["ok"]) != 1:
        raise Exception("Cluster connection is not okay!")
    
    book_lists = database.get_collection(COLLECTION_NAME)
    app.book_dal = BookDAL(book_lists)

    yield
    client.close()

app = FastAPI(lifespan=lifespan, debug=DEBUG)

# Add CORS middleware
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],  
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

def get_dal(request: Request) -> BookDAL:
    return request.app.book_dal

@app.get("/")
def home():
    return {"message": "Welcome to the BookStore CRUD API"} 

@app.post("/books", response_model=Book)
async def create_book(book: BookCreate, dal: BookDAL = Depends(get_dal)):
    return await dal.create_book(book)

@app.get("/books", response_model=list[Book])
async def get_books(dal: BookDAL = Depends(get_dal)):
    return await dal.get_all_books()

@app.get("/books/{book_id}", response_model=Book)
async def get_book(book_id: str, dal: BookDAL = Depends(get_dal)):
    book = await dal.get_book_by_id(book_id)
    if not book:
        raise HTTPException(status_code=404, detail="Book not found")
    return book

@app.patch("/books/{book_id}", response_model=Book)
async def update_book(book_id: str, updates: BookUpdate, dal: BookDAL = Depends(get_dal)):
    book = await dal.update_book(book_id, updates)
    if not book:
        raise HTTPException(status_code=404, detail="Book not found or no updates applied")
    return book

@app.delete("/books/{book_id}")
async def delete_book(book_id: str, dal: BookDAL = Depends(get_dal)):
    success = await dal.delete_book(book_id)
    if not success:
        raise HTTPException(status_code=404, detail="Book not found")
    return {"message": "Book deleted successfully"}

Connect to MongoDB

  • Create a .env file and add your MongoDB connection string

MONGODB_URI='mongodb+srv://<db_username>:<db_password>@cluster0.kzk0k.mongodb.net/<db_name>?retryWrites=true&w=majority&appName=Cluste
  • Use Motor (async MongoDB driver for Python) in the database.py file

from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo import ReturnDocument
from uuid import uuid4

from models import Book, BookCreate, BookUpdate

class BookDAL:
    def __init__(self, collection: AsyncIOMotorCollection):
        self.collection = collection

    async def create_book(self, book_data: BookCreate):
        result = await self.collection.insert_one(book_data.dict())
        doc = await self.collection.find_one({"_id": result.inserted_id})
        return Book.from_doc(doc)

    async def get_all_books(self):
        books = []
        async for doc in self.collection.find():
            books.append(Book.from_doc(doc))
        return books

    async def get_book_by_id(self, book_id: str):
        doc = await self.collection.find_one({"_id": ObjectId(book_id)})
        return Book.from_doc(doc) if doc else None

    async def update_book(self, book_id: str, update_data: BookUpdate):
        update_dict = {k: v for k, v in update_data.dict().items() if v is not None}
        result = await self.collection.find_one_and_update(
            {"_id": ObjectId(book_id)},
            {"$set": update_dict},
            return_document=ReturnDocument.AFTER
        )
        return Book.from_doc(result) if result else None

    async def delete_book(self, book_id: str):
        result = await self.collection.delete_one({"_id": ObjectId(book_id)})
        return result.deleted_count == 1

3.5. Run and test the API with Swagger UI

uvicorn main:app --reload

Creating the Frontend with React

Set up React project with TailwindCSS

  • Using Vite

cd frontend
npm create vite@latest . – –template react
npm install
npm run dev
  • Install Axios

Axios is a popular interface for making common requests like GET, POST, PUT, PATCH, DELETE and more. Axios allows us to handle HTTP requests asynchronously and cleanly. Axios has straightforward syntax and is ease to use in JavaScript projects.

cd frontend
npm install axios

Build the Bookstore CRUD App

  • In the src folder create a components folder and create two .jsx files:

    • BookForm.jsx

// Import all dependecies
import { useState, useEffect } from "react";
import axios from "axios";

// Define the API URL
const API_URL = "http://localhost:8000/books";

export default function BookForm({ onBookAdded,  editingBook, onCancelEdit  }) {
  // Define and set state
  const [book, setBook] = useState({
    title: "",
    author: "",
    summary: "",
  });
	// Updates the form fields
  useEffect(() => {
    if (editingBook) {
      setBook({
        title: editingBook.title,
        author: editingBook.author,
        summary: editingBook.summary,
      });
    } else {
        setBook({
            title: "",
            author: "",
            summary: "",
        })
    }
  }, [editingBook]); 
	// Handles changes as user inputs book details
  const handleChange = (e) => {
    setBook({ ...book, [e.target.name]: e.target.value });
  };
	// Handles form submission
  const handleSubmit = async (e) => {
    // creates a payload from the current book state
    const payload = {
      ...book,
    };
		// Sends a PATCH request if true,
		// Else send a POST request to create new book
    if (editingBook) {
      await axios.patch(`${API_URL}/${editingBook.id}`, payload);
      onCancelEdit();
    } else {
      await axios.post(API_URL, payload);
    }
    // After submission, reloads the book list
    onBookAdded();
    // Clears form input
    setBook({ title: "", author: "", summary: "" });
  };

  return (
    <form onSubmit={handleSubmit} 
    className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
      {["title", "author", "summary"].map((field) => (
        <div className="mb-4" key={field}>
            <label className="block text-gray-700 text-sm font-bold mb-2 capitalize" htmlFor={field}>
            {field}
            </label>
            <input
            className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline placeholder-gray-400"
            key={field}
            name={field}
            placeholder={field}
            value={book[field]}
            onChange={handleChange}
            required
            />
        </div>
      ))}
      <div className="flex items-center justify-between">
      <button
          className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
          type="submit"
        >
          {editingBook ? "Update Book" : "Add Book"}
        </button>
        {editingBook && (
          <button
            type="button"
            onClick={onCancelEdit}
            className="ml-4 bg-gray-400 hover:bg-gray-600 text-white font-bold py-2 px-4 rounded focus:outline-none focus:shadow-outline"
          >
            Cancel
          </button>
        )}
      </div>
    </form>
  );
}
  • BookList.jsx

// Import all dependecies
import { useEffect, useState } from "react";
import axios from "axios";
// import BookForm component
import BookForm from "./BookForm"
// Define the API URL
const API_URL = "http://localhost:8000/books";

export default function BookList() {

// Define and set state
  const [books, setBooks] = useState([]);
  
// Get books from database
  const loadBooks = async () => {
    const res = await axios.get(API_URL);
    setBooks(res.data);
  };
// Deletes a book
  const handleDelete = async (id) => {
    await axios.delete(`${API_URL}/${id}`);
    loadBooks();
  };
  
// Load all books when component is first mounted
  useEffect(() => {
    loadBooks();
  }, []);

  return (
    <div className="max-w-3xl mx-auto p-4">
      <h2 className="text-2xl font-bold mb-4 text-center">Bookstore CRUD App</h2>
      <BookForm onBookAdded={loadBooks} />
      <div className="bg-white shadow-md rounded px-8 pt-6 pb-8 mb-4">
        <h3 className="text-xl font-semibold mb-4">Books</h3>
        {books.length === 0 ? (
          <p className="text-gray-600">No books available.</p>
        ) : (
            <div className="overflow-x-auto">
            <table className="min-w-full divide-y divide-gray-200">
              <thead className="bg-gray-50">
                <tr>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Author</th>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Summary</th>
                  <th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
                </tr>
              </thead>
              <tbody className="bg-white divide-y divide-gray-200">
                {books.map((book) => (
                  <tr key={book.id}>
                    <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{book.title}</td>
                    <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{book.author}</td>
                    <td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{book.summary}</td>
                    <td className="px-6 py-4 whitespace-nowrap text-sm">
                      <button
                        onClick={() => handleDelete(book.id)}
                        className="text-red-500 hover:text-red-700"
                      >
                        Delete
                      </button>
                    </td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        )}
      </div>
    </div>
  );
}
  • App.jsx

import BookList from "./components/BookList";
import "./App.css"

// Render the app
function App() {
  return (
    <div className="min-h-screen bg-gray-100 py-10 font-serif">
      <div>
        <BookList />
      </div>
    </div>
  );
}

export default App;

Run the Application

  • Start the Backend server first

uvicorn main:app --reload
  • Start the Frontend Application

npm run dev

Conclusion

Well-done! You have a simple Bookstore CRUD app built with the FARM stack. You can add additional features like authentication, pagination and search features. Feel free tp copy the code or clone the Github repository. If you found this guide helpful, please consider sharing and connecting with me.

Learning Resources

Course image
Become a Python Backend Engineeer today

All-in-one Python course for learning backend engineering with Python. This comprehensive course is designed for Python developers seeking proficiency in Python.

Start Learning Now

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board

Backend Tips, Every week

Backend Tips, Every week