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.