DEV Community

StackPuz
StackPuz

Posted on • Originally published at blog.stackpuz.com on

Building a React CRUD App with a FastAPI

React CRUD App with a FastAPI

In this article, we'll walk you through developing a full-stack CRUD application, focusing on how React handles the creation, reading, updating, and deletion of data on the front end, while FastAPI manages these operations on the back end. By integrating these frameworks, you'll see how they work together to build a fast and efficient app that performs all essential CRUD functions smoothly.

Prerequisites

  • Node.js
  • Python 3.10
  • MySQL

Setup React project

npm create vite@4.4.0 view -- --template react
cd view
npm install react-router-dom@5 axios
Enter fullscreen mode Exit fullscreen mode

React project structure

├─ index.html
├─ public
│  └─ css
│     └─ style.css
└─ src
   ├─ components
   │  └─ product
   │     ├─ Create.jsx
   │     ├─ Delete.jsx
   │     ├─ Detail.jsx
   │     ├─ Edit.jsx
   │     ├─ Index.jsx
   │     └─ Service.js
   ├─ history.js
   ├─ http.js
   ├─ main.jsx
   ├─ router.jsx
   └─ App.jsx
Enter fullscreen mode Exit fullscreen mode

React project files

main.jsx

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

ReactDOM.createRoot(document.getElementById('root')).render(
  <App />
)
Enter fullscreen mode Exit fullscreen mode

The main.jsx file is the entry point for a React app. It imports React, ReactDOM, and the App component. The ReactDOM.createRoot method is used to render the App component into the HTML element with the ID root.

App.jsx

import React, { useState, useEffect } from 'react'
import { Router, Link } from 'react-router-dom'
import history from './history'
import Route from './router'

export default function App() {
  return (
    <Router history={history}>
      <Route />
    </Router>
  )
}
Enter fullscreen mode Exit fullscreen mode

The App.jsx file sets up routing for a React app. The App component wraps the application in a Router using the custom history, and renders the Route component for handling routes.

history.js

import { createBrowserHistory } from 'history'

export default createBrowserHistory()

Enter fullscreen mode Exit fullscreen mode

The history.js file exports a custom browser history object created with createBrowserHistory for managing navigation in a React app.

router.jsx

import React, { Suspense, lazy } from 'react'
import { Switch, Route, Redirect } from 'react-router-dom'

export default function AppRoute(props) {
  return (
    <Suspense fallback={''}>
      <Switch>
        <Route path="/" component={(p) => <Redirect to="/product" /> } exact />
        <Route path="/product" component={lazy(() => import('./components/product/Index'))} exact />
        <Route path="/product/create" component={lazy(() => import('./components/product/Create'))} exact />
        <Route path="/product/:id/" component={lazy(() => import('./components/product/Detail'))} exact />
        <Route path="/product/edit/:id/" component={lazy(() => import('./components/product/Edit'))} exact />
        <Route path="/product/delete/:id/" component={lazy(() => import('./components/product/Delete'))} exact />
      </Switch>
    </Suspense>
  )
}
Enter fullscreen mode Exit fullscreen mode

The router.jsx file sets up routing for a React app with lazy-loaded components. It uses Suspense to handle loading status, and Switch to define routes. The root path redirects to /product, and specific routes handle product-related pages like create, detail, edit, and delete.

http.js

import axios from 'axios'

let http = axios.create({
  baseURL: 'http://localhost:8000/api',
  headers: {
    'Content-type': 'application/json'
  }
})

export default http
Enter fullscreen mode Exit fullscreen mode

The http.js file configures and exports an Axios instance with a centralized base URL, which is a standard practice for managing API endpoints and default headers set to application/json.

Create.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductCreate(props) {

  const [product, setProduct] = useState({})

  function create(e) {
    e.preventDefault()
    Service.create(product).then(() => {
      props.history.push('/product')
    }).catch((e) => {
      alert(e.response.data)
    })
  }

  function onChange(e) {
    let data = { ...product }
    data[e.target.name] = e.target.value
    setProduct(data)
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post" onSubmit={create}>
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">Name</label>
                <input id="product_name" name="name" className="form-control" onChange={onChange} value={product.name ?? '' } maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">Price</label>
                <input id="product_price" name="price" className="form-control" onChange={onChange} value={product.price ?? '' } type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">Cancel</Link>
                <button className="btn btn-primary">Submit</button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The create.jsx file defines a ProductCreate component for adding a new product. It uses useState to manage form data, and handles form submission with create(e) that sends data via Service.create and redirects on success. The form includes fields for product name and price, with a cancel link and submit button.

Delete.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductDelete(props) {

  const [product, setProduct] = useState({})

  useEffect(() => {
    get()
  }, [props.match.params.id])

  function get() {
    return Service.delete(props.match.params.id).then(response => {
      setProduct(response.data)
    }).catch(e => {
      alert(e.response.data)
    })
  }

  function remove(e) {
    e.preventDefault()
    Service.delete(props.match.params.id, product).then(() => {
      props.history.push('/product')
    }).catch((e) => {
      alert(e.response.data)
    })
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post" onSubmit={remove}>
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_id">Id</label>
                <input readOnly id="product_id" name="id" className="form-control" value={product.id ?? '' } type="number" required />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">Name</label>
                <input readOnly id="product_name" name="name" className="form-control" value={product.name ?? '' } maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">Price</label>
                <input readOnly id="product_price" name="price" className="form-control" value={product.price ?? '' } type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">Cancel</Link>
                <button className="btn btn-danger">Delete</button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The Delete.jsx file defines a ProductDelete component for deleting a product. It fetches product details using Service.delete and displays them in a read-only form. The component uses useEffect to load product data based on the product ID from the route parameters. The remove(e) function handles deletion and redirects to /product upon success.

Detail.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductDetail(props) {

  const [product, setProduct] = useState({})

  useEffect(() => {
    get()
  }, [props.match.params.id])

  function get() {
    return Service.get(props.match.params.id).then(response => {
      setProduct(response.data)
    }).catch(e => {
      alert(e.response.data)
    })
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post">
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_id">Id</label>
                <input readOnly id="product_id" name="id" className="form-control" value={product.id ?? '' } type="number" required />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">Name</label>
                <input readOnly id="product_name" name="name" className="form-control" value={product.name ?? '' } maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">Price</label>
                <input readOnly id="product_price" name="price" className="form-control" value={product.price ?? '' } type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">Back</Link>
                <Link className="btn btn-primary" to={`/product/edit/${product.id}`}>Edit</Link>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The Detail.jsx file defines a ProductDetail component that displays details of a product. It fetches product data using Service.get based on the product ID from the route parameters and displays it in a read-only form, and provides links to go back to the product list or to edit the product.

Edit.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductEdit(props) {

  const [product, setProduct] = useState({})

  useEffect(() => {
    get()
  }, [props.match.params.id])

  function get() {
    return Service.edit(props.match.params.id).then(response => {
      setProduct(response.data)
    }).catch(e => {
      alert(e.response.data)
    })
  }

  function edit(e) {
    e.preventDefault()
    Service.edit(props.match.params.id, product).then(() => {
      props.history.push('/product')
    }).catch((e) => {
      alert(e.response.data)
    })
  }

  function onChange(e) {
    let data = { ...product }
    data[e.target.name] = e.target.value
    setProduct(data)
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <form method="post" onSubmit={edit}>
            <div className="row">
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_id">Id</label>
                <input readOnly id="product_id" name="id" className="form-control" onChange={onChange} value={product.id ?? '' } type="number" required />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_name">Name</label>
                <input id="product_name" name="name" className="form-control" onChange={onChange} value={product.name ?? '' } maxLength="50" />
              </div>
              <div className="mb-3 col-md-6 col-lg-4">
                <label className="form-label" htmlFor="product_price">Price</label>
                <input id="product_price" name="price" className="form-control" onChange={onChange} value={product.price ?? '' } type="number" />
              </div>
              <div className="col-12">
                <Link className="btn btn-secondary" to="/product">Cancel</Link>
                <button className="btn btn-primary">Submit</button>
              </div>
            </div>
          </form>
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The Edit.jsx file defines a ProductEdit component for updating product details. It fetches the current product data using Service.edit and populates a form with this data. The form allows users to modify the product's name and price. On form submission, the edit(e) function updates the product via Service.edit and redirects to the product list on success.

Index.jsx

import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Service from './Service'

export default function ProductIndex(props) {

  const [products, setProducts] = useState([])

  useEffect(() => {
    get()
  }, [props.location])

  function get() {
    Service.get().then(response => {
      setProducts(response.data)
    }).catch(e => {
      alert(e.response.data)
    })
  }

  return (
    <div className="container">
      <div className="row">
        <div className="col">
          <table className="table table-striped table-hover">
            <thead>
              <tr>
                <th>Id</th>
                <th>Name</th>
                <th>Price</th>
                <th>Actions</th>
              </tr>
            </thead>
            <tbody>
              {products.map((product, index) =>
              <tr key={index}>
                <td className="text-center">{product.id}</td>
                <td>{product.name}</td>
                <td className="text-center">{product.price}</td>
                <td className="text-center">
                  <Link className="btn btn-secondary" to={`/product/${product.id}`} title="View"><i className="fa fa-eye"></i></Link>
                  <Link className="btn btn-primary" to={`/product/edit/${product.id}`} title="Edit"><i className="fa fa-pencil"></i></Link>
                  <Link className="btn btn-danger" to={`/product/delete/${product.id}`} title="Delete"><i className="fa fa-times"></i></Link>
                </td>
              </tr>
              )}
            </tbody>
          </table>
          <Link className="btn btn-primary" to="/product/create">Create</Link>
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

The Index.jsx file defines a ProductIndex component that displays a list of products in a table. It fetches product data using Service.get and updates the list on component mount. The table shows product ID, name, and price, with action buttons for viewing, editing, and deleting each product. It also includes a link to create a new product.

Service.js

import http from '../../http'

export default {
  get(id) {
    if (id) {
      return http.get(`/products/${id}`)
    }
    else {
      return http.get('/products' + location.search)
    }
  },
  create(data) {
    if (data) {
      return http.post('/products', data)
    }
    else {
      return http.get('/products/create')
    }
  },
  edit(id, data) {
    if (data) {
      return http.put(`/products/${id}`, data)
    }
    else {
      return http.get(`/products/${id}`)
    }
  },
  delete(id, data) {
    if (data) {
      return http.delete(`/products/${id}`)
    }
    else {
      return http.get(`/products/${id}`)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The Service.js file defines API methods for handling product operations. It uses an http instance for making requests:

  • get(id) Retrieves a single product by ID or all products if no ID is provided.
  • create(data) Creates a new product with the provided data or fetches the creation form if no data is provided.
  • edit(id, data) Updates a product by ID with the provided data or fetches the product details if no data is provided.
  • delete(id, data) Deletes a product by ID or fetches the product details if no data is provided.

style.css

.container {
  margin-top: 2em;
}

.btn {
  margin-right: 0.25em;
}

Enter fullscreen mode Exit fullscreen mode

The CSS adjusts the layout by adding space above the container and spacing out buttons horizontally.

index.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
  <link href="/css/style.css" rel="stylesheet">
</head>
<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>
</html>

Enter fullscreen mode Exit fullscreen mode

The HTML serves as the main entry point for an React application, including Bootstrap for styling, Font Awesome for icons, It features a div with the ID root where the React app will render.

Setup FastAPI project

pip install fastapi sqlalchemy pymysql uvicorn python-dotenv
Enter fullscreen mode Exit fullscreen mode

Create a testing database named "example" and execute the database.sql file to import the table and data.

FastAPI Project structure

├─ .env
└─ app
   ├─ db.py
   ├─ main.py
   ├─ models
   │  └─ product.py
   ├─ routers
   │  └─ product.py
   ├─ schemas
   │  └─ product.py
   └─ __init__.py
Enter fullscreen mode Exit fullscreen mode

__init__.py is used to mark the directory as a Python package, allowing it to be recognized as a module that can contain submodules and facilitate imports.

FastAPI Project files

.env

DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=example
DB_USER=root
DB_PASSWORD=
Enter fullscreen mode Exit fullscreen mode

This file holds the configuration details for connecting to the database.

db.py

import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

load_dotenv()
url = f"mysql+pymysql://{os.getenv('DB_USER')}:{os.getenv('DB_PASSWORD')}@{os.getenv('DB_HOST')}:{os.getenv('DB_PORT')}/{os.getenv('DB_DATABASE')}"
engine = create_engine(url)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
Enter fullscreen mode Exit fullscreen mode

The db.py module sets up the database connection for a FastAPI application using SQLAlchemy. It loads database credentials from environment variables, creates a SQLAlchemy engine for a MySQL database, and establishes a session factory for database interactions. The get_db function provides a database session that can be used in dependency injection, ensuring proper session management by closing the session after use.

models\product.py

from sqlalchemy import *
from app.db import Base

class Product(Base):
    __tablename__ = "Product"
    id = Column(INTEGER, primary_key=True)
    name = Column(VARCHAR)
    price = Column(DECIMAL)
Enter fullscreen mode Exit fullscreen mode

The models\product.py module defines a SQLAlchemy model for the Product table, mapping its columns for the product's ID, name, and price. It extends the Base class from the db module, enabling easy interaction with product data in the database.

schemas\product.py

from pydantic import BaseModel
from decimal import Decimal

class ProductCreate(BaseModel):
    name: str
    price: Decimal

class ProductUpdate(BaseModel):
    name: str
    price: Decimal
Enter fullscreen mode Exit fullscreen mode

The schemas/product.py module defines Pydantic models for validating and serializing product data in a FastAPI application. It includes two classes: ProductCreate, which is used for creating new products by specifying the name and price, and ProductUpdate, which is designed for updating existing product information. Both classes ensure that the provided data adheres to the expected types.

routers\product.py

from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app.db import get_db
from app.models.product import Product
from app.schemas.product import ProductCreate, ProductUpdate

router = APIRouter()

@router.get("/products")
def index(db: Session = Depends(get_db)):
    return db.query(Product).all()

@router.get("/products/{id}")
def get(id: int, db: Session = Depends(get_db)):
    return db.query(Product).filter(Product.id == id).first()

@router.post("/products")
def create(payload: ProductCreate, db: Session = Depends(get_db)):
    product = Product(**payload.model_dump())
    db.add(product)
    db.commit()
    db.refresh(product)
    return product

@router.put("/products/{id}")
def update(id: int, payload: ProductUpdate, db: Session = Depends(get_db)):
    product = db.query(Product).filter(Product.id == id).first()
    product.name = payload.name
    product.price = payload.price
    db.commit()
    db.refresh(product)
    return product

@router.delete("/products/{id}")
def delete(id: int, db: Session = Depends(get_db)):
    db.query(Product).filter(Product.id == id).delete()
    db.commit()
Enter fullscreen mode Exit fullscreen mode

The routers/product.py module defines a FastAPI router for managing product-related API endpoints. It provides the following functionalities:

  • index function returns a list of all products from the database.
  • get function fetches a specific product by its ID.
  • create function adds a new product to the database using the data provided in the ProductCreate schema.
  • update function modifies an existing product's details based on its ID and the provided ProductUpdate data.
  • delete function removes a product from the database by its ID.

All operations depend on a database session obtained through the get_db dependency, ensuring proper session management for each request.

main.py

from fastapi import FastAPI
from fastapi.responses import FileResponse
from fastapi.middleware.cors import CORSMiddleware
from app.routers.product import router

app = FastAPI()
app.include_router(router, prefix="/api")
@app.get("/")
async def read_index():
    return FileResponse("app/static/index.html")

app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

if __name__ == " __main__":
    uvicorn.run(app, host="127.0.0.1")
Enter fullscreen mode Exit fullscreen mode

The main.py module serves as the entry point for a FastAPI application. It initializes the FastAPI app and includes a router for product-related endpoints under the /api prefix. The module also defines a root endpoint that serves an index.html file from the static directory. CORS middleware is added to allow requests from any origin, enabling cross-origin resource sharing. Finally, the application is configured to run with Uvicorn when executed directly, listening on 127.0.0.1.

Run projects

Run React project

npm run dev
Enter fullscreen mode Exit fullscreen mode

Run FastAPI project

uvicorn app.main:app
Enter fullscreen mode Exit fullscreen mode

Open the web browser and goto http://localhost:5173

You will find this product list page.

list page

Testing

Click the "View" button to see the product details page.

details page

Click the "Edit" button to modify the product and update its details.

edit page

Click the "Submit" button to save the updated product details.

updated data

Click the "Create" button to add a new product and input its details.

create page

Click the "Submit" button to save the new product.

created product

Click the "Delete" button to remove the previously created product.

delete page

Click the "Delete" button to confirm the removal of this product.

deleted product

Conclusion

In conclusion, we have learned how to create a basic React project using JSX syntax to build views and manage routing, while setting up a FastAPI server as the backend. By utilizing SQLAlchemy for database operations, we've developed a dynamic front-end that seamlessly interacts with a robust backend, providing a strong foundation for modern, full-stack web applications.

Source code: https://github.com/stackpuz/Example-CRUD-React-18-FastAPI

Create a React CRUD App in Minutes: https://stackpuz.com

Top comments (0)