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.