DEV Community

Gilbert Ngeno
Gilbert Ngeno

Posted on

Part 1: Building Your First Full-Stack CRUD App with FastAPI and React.js

A Beginner's Guide to Creating, Connecting, and Deploying a Web Application
Have you ever wondered how web applications work behind the scenes? How data gets created, displayed, updated, and deleted? In this tutorial, I'll guide you through building a complete CRUD (Create, Read, Update, Delete) application using FastAPI for the backend and React.js for the frontend. We'll even deploy it to the internet so you can share it with others!

What You'll Build
You'll create a simple item management system where you can:

  • Add new items with names and descriptions
  • View all items in a list
  • Edit existing items
  • Delete items you no longer need

Prerequisites
Before we start, make sure you have:

  • Basic knowledge of Python and JavaScript
  • Python 3.7+ installed on your computer
  • Node.js and npm installed
  • A text editor (VS Code recommended)

Part 1: Setting Up the Backend with FastAPI
FastAPI is a modern, fast web framework for building APIs with Python. It's perfect for beginners because it provides automatic interactive documentation.

Step-by-Step Backend Setup

  1. Create Project Structure

Open your terminal and run these commands:

# Create a main project directory
mkdir crud-app
# Move into the directory
cd crud-app
# Create a virtual environment (isolated Python environment)
python -m venv venv
Enter fullscreen mode Exit fullscreen mode

Explanation:

mkdir creates a new directory/folder

cd changes your current directory

python -m venv venv creates a virtual environment named "venv" that keeps your project's dependencies separate from other Python projects

  1. Activate Virtual Environment
# On macOS/Linux:
source venv/bin/activate
# On Windows:
venv\Scripts\activate
Enter fullscreen mode Exit fullscreen mode

Explanation:
Activating the virtual environment ensures that any packages we install are only available for this specific project. You'll know it's activated when you see (venv) at the beginning of your terminal prompt.

  1. Install Required Packages
pip install fastapi uvicorn sqlalchemy aiosqlite
Enter fullscreen mode Exit fullscreen mode

Explanation:

fastapi: The main web framework

uvicorn: A server that will run our FastAPI application

sqlalchemy: A database toolkit that helps us work with databases

aiosqlite: Allows async database operations with SQLite

  1. Create Backend Files

Create a backend directory and these files inside it:

crud-app/
├── backend/
│   ├── main.py
│   ├── database.py
│   ├── models.py
│   └── schemas.py
Enter fullscreen mode Exit fullscreen mode
  1. Set Up the Database (database.py)
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker

# Database URL for SQLite (it will create a file called test.db)
SQLALCHEMY_DATABASE_URL = "sqlite:///./test.db"

# Create the database engine
engine = create_engine(
    SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
)

# Create a SessionLocal class for database sessions
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# Create a Base class for our models
Base = declarative_base()
Enter fullscreen mode Exit fullscreen mode

Explanation:

We're using SQLite, a simple file-based database, for simplicity

The engine is responsible for connecting to the database

SessionLocal will be used to create database sessions

Base is a class that our models will inherit from

  1. Create the Item Model (models.py)
from sqlalchemy import Column, Integer, String
from database import Base

class Item(Base):
    __tablename__ = "items"

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    description = Column(String, index=True)

Enter fullscreen mode Exit fullscreen mode

Explanation:

We're creating a table called "items" with three columns

id is our primary key (unique identifier for each item)

name and description will store text data

index=True makes searching faster for these columns

  1. Create Pydantic Schemas (schemas.py)
from pydantic import BaseModel

# Base schema with common attributes
class ItemBase(BaseModel):
    name: str
    description: str = None

# Schema for creating a new item
class ItemCreate(ItemBase):
    pass

# Schema for reading/returning an item (includes id)
class Item(ItemBase):
    id: int

    class Config:
        orm_mode = True
Enter fullscreen mode Exit fullscreen mode

Explanation:

Schemas define the structure of our data when sending/receiving from the API

ItemCreate is used when creating a new item (doesn't need an ID yet)

Item is used when returning data (includes the ID)

orm_mode = True allows converting from database models to Pydantic models

  1. Create the Main FastAPI App (main.py)
from fastapi import FastAPI, Depends, HTTPException
from sqlalchemy.orm import Session
import models, schemas
from database import SessionLocal, engine
from fastapi.middleware.cors import CORSMiddleware

# Create all tables in the database
models.Base.metadata.create_all(bind=engine)

# Create the FastAPI application
app = FastAPI()

# Add CORS middleware to allow React frontend to connect
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],  # React's default port
    allow_credentials=True,
    allow_methods=["*"],  # Allow all HTTP methods
    allow_headers=["*"],  # Allow all headers
)

# Dependency to get database session
def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# CREATE - Add a new item
@app.post("/items/", response_model=schemas.Item)
def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)):
    db_item = models.Item(**item.dict())
    db.add(db_item)
    db.commit()
    db.refresh(db_item)
    return db_item

# READ - Get all items
@app.get("/items/", response_model=list[schemas.Item])
def read_items(skip: int = 0, limit: int = 100, db: Session = Depends(get_db)):
    items = db.query(models.Item).offset(skip).limit(limit).all()
    return items

# READ - Get a single item by ID
@app.get("/items/{item_id}", response_model=schemas.Item)
def read_item(item_id: int, db: Session = Depends(get_db)):
    item = db.query(models.Item).filter(models.Item.id == item_id).first()
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")
    return item

# UPDATE - Update an existing item
@app.put("/items/{item_id}", response_model=schemas.Item)
def update_item(item_id: int, item: schemas.ItemCreate, db: Session = Depends(get_db)):
    db_item = db.query(models.Item).filter(models.Item.id == item_id).first()
    if db_item is None:
        raise HTTPException(status_code=404, detail="Item not found")

    for field, value in item.dict().items():
        setattr(db_item, field, value)

    db.commit()
    db.refresh(db_item)
    return db_item

# DELETE - Remove an item
@app.delete("/items/{item_id}")
def delete_item(item_id: int, db: Session = Depends(get_db)):
    item = db.query(models.Item).filter(models.Item.id == item_id).first()
    if item is None:
        raise HTTPException(status_code=404, detail="Item not found")

    db.delete(item)
    db.commit()
    return {"message": "Item deleted successfully"}
Enter fullscreen mode Exit fullscreen mode

Explanation:

We create endpoints for all CRUD operations (Create, Read, Update, Delete)

Each endpoint has a specific HTTP method (@app.post, @app.get, etc.)

The response_model parameter ensures our responses match the expected schema

Depends(get_db) provides a database session for each request

CORS middleware allows our React frontend to communicate with the backend

  1. Run the Backend Server
cd backend
uvicorn main:app --reload
Enter fullscreen mode Exit fullscreen mode

Explanation:

uvicorn is the server that runs our FastAPI application

main:app tells uvicorn to look for the app object in the main.py file

--reload enables automatic reloading when we make code changes

Your API is now running at http://localhost:8000! Visit http://localhost:8000/docs to see the interactive documentation automatically generated by FastAPI.

Part 2: Building the Frontend with React.js
React is a popular JavaScript library for building user interfaces. We'll create a simple interface to interact with our backend API.

Step-by-Step Frontend Setup

  1. Create React App

From the project root directory, run:

# Create a new React application called "frontend"
npx create-react-app frontend
cd frontend
Enter fullscreen mode Exit fullscreen mode

Explanation:

npx runs commands from npm packages without installing them globally

create-react-app is a tool that sets up a modern React application with all the necessary configuration

  1. Install Axios for API Calls
npm install axios
Enter fullscreen mode Exit fullscreen mode

Explanation:

Axios is a library that makes it easier to send HTTP requests to our backend API

React doesn't have built-in HTTP request capabilities, so we need a library like Axios

  1. Replace the Contents of App.js

Open src/App.js and replace its contents with:

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';

// Base URL for our backend API
const API_BASE = "http://localhost:8000";

function App() {
  // State variables to manage our data
  const [items, setItems] = useState([]);
  const [name, setName] = useState('');
  const [description, setDescription] = useState('');
  const [editingId, setEditingId] = useState(null);

  // useEffect hook runs when the component mounts
  useEffect(() => {
    fetchItems();
  }, []);

  // Function to fetch all items from the backend
  const fetchItems = async () => {
    try {
      const response = await axios.get(`${API_BASE}/items/`);
      setItems(response.data);
    } catch (error) {
      console.error("Error fetching items:", error);
    }
  };

  // Function to create a new item
  const createItem = async () => {
    try {
      const response = await axios.post(`${API_BASE}/items/`, {
        name,
        description
      });
      setItems([...items, response.data]);
      setName('');
      setDescription('');
    } catch (error) {
      console.error("Error creating item:", error);
    }
  };

  // Function to update an existing item
  const updateItem = async () => {
    try {
      const response = await axios.put(`${API_BASE}/items/${editingId}`, {
        name,
        description
      });
      setItems(items.map(item => item.id === editingId ? response.data : item));
      setEditingId(null);
      setName('');
      setDescription('');
    } catch (error) {
      console.error("Error updating item:", error);
    }
  };

  // Function to delete an item
  const deleteItem = async (id) => {
    try {
      await axios.delete(`${API_BASE}/items/${id}`);
      setItems(items.filter(item => item.id !== id));
    } catch (error) {
      console.error("Error deleting item:", error);
    }
  };

  // Function to start editing an item
  const startEdit = (item) => {
    setEditingId(item.id);
    setName(item.name);
    setDescription(item.description);
  };

  // Function to cancel editing
  const cancelEdit = () => {
    setEditingId(null);
    setName('');
    setDescription('');
  };

  return (
    <div className="App">
      <h1>CRUD App with FastAPI and React</h1>

      <div className="item-form">
        <h2>{editingId ? 'Edit Item' : 'Add New Item'}</h2>
        <input
          type="text"
          placeholder="Name"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <input
          type="text"
          placeholder="Description"
          value={description}
          onChange={(e) => setDescription(e.target.value)}
        />
        {editingId ? (
          <>
            <button onClick={updateItem}>Update</button>
            <button onClick={cancelEdit}>Cancel</button>
          </>
        ) : (
          <button onClick={createItem}>Create</button>
        )}
      </div>

      <div className="items-list">
        <h2>Items</h2>
        {items.length === 0 ? (
          <p>No items found</p>
        ) : (
          <ul>
            {items.map(item => (
              <li key={item.id}>
                <h3>{item.name}</h3>
                <p>{item.description}</p>
                <button onClick={() => startEdit(item)}>Edit</button>
                <button onClick={() => deleteItem(item.id)}>Delete</button>
              </li>
            ))}
          </ul>
        )}
      </div>
    </div>
  );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Explanation:

We use React hooks (useState, useEffect) to manage state and side effects

*API_BASE * points to our backend server

Each function makes an API call using Axios and updates the UI accordingly

The form changes between "Create" and "Edit" mode based on whether we're editing an existing item

  1. Update the CSS (App.css)

Replace the contents of src/App.css with:

.App {
  text-align: center;
  padding: 20px;
  font-family: Arial, sans-serif;
}

.item-form {
  margin: 20px 0;
  padding: 20px;
  border: 1px solid #ccc;
  border-radius: 5px;
  max-width: 500px;
  margin-left: auto;
  margin-right: auto;
  background-color: #f9f9f9;
}

.item-form input {
  display: block;
  margin: 10px auto;
  padding: 10px;
  width: 80%;
  border: 1px solid #ddd;
  border-radius: 4px;
}

.item-form button {
  margin: 5px;
  padding: 10px 15px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  background-color: #4CAF50;
  color: white;
}

.item-form button:hover {
  background-color: #45a049;
}

.items-list ul {
  list-style-type: none;
  padding: 0;
  max-width: 600px;
  margin: 0 auto;
}

.items-list li {
  border: 1px solid #eee;
  margin: 10px 0;
  padding: 15px;
  border-radius: 5px;
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.items-list button {
  margin: 0 5px;
  padding: 5px 10px;
  border: none;
  border-radius: 3px;
  cursor: pointer;
}

.items-list button:first-child {
  background-color: #2196F3;
  color: white;
}

.items-list button:last-child {
  background-color: #f44336;
  color: white;
}

.items-list button:hover {
  opacity: 0.8;
}
Enter fullscreen mode Exit fullscreen mode
  1. Run the React Application
npm start
Enter fullscreen mode Exit fullscreen mode

Your React app will open at http://localhost:3000. You should now be able to add, view, edit, and delete items!

Part 3: Connecting Backend and Frontend
The magic happens through HTTP requests between the frontend and backend:

Frontend (React): Makes requests using Axios to specific URLs

Backend (FastAPI): Listens for these requests, processes them, and returns responses

Frontend (React): Updates the UI based on the responses

Key Connection Points:

API_BASE Constant: const API_BASE = "http://localhost:8000"; tells the frontend where to find the backend

CORS Middleware: The CORSMiddleware in FastAPI allows the frontend to make requests from a different domain (localhost:3000) to the backend (localhost:8000)

HTTP Methods:

  • axios.get() → @app.get() (Read)
  • axios.post() → @app.post() (Create)
  • axios.put() → @app.put() (Update)
  • axios.delete() → @app.delete() (Delete)

Data Flow:

  • User interacts with React UI
  • React sends HTTP request to FastAPI
  • FastAPI processes request (reads/writes to database)
  • FastAPI sends response back to React
  • React updates UI based on response

Part 4: Deploying to the Internet
Now let's make your application accessible to anyone on the internet! We'll use Heroku, a platform that makes deployment easy.

Backend Deployment

  1. Install Heroku CLI

Download and install from heroku.com

  1. Login to Heroku
heroku login
Enter fullscreen mode Exit fullscreen mode
  1. Create requirements.txt
cd backend
pip freeze > requirements.txt
Enter fullscreen mode Exit fullscreen mode

Explanation: This file lists all Python packages needed for your application. Heroku uses it to install dependencies.

  1. Create Procfile

Create a file named Procfile (no extension) in the backend directory with:

web: uvicorn main:app --host=0.0.0.0 --port=$PORT
Enter fullscreen mode Exit fullscreen mode

Explanation: Tells Heroku how to start your application. The $PORT is provided by Heroku.

  1. Initialize Git and Deploy
# Initialize git repository
git init
# Add all files to git
git add .
# Commit files
git commit -m "Initial commit"
# Create Heroku app
heroku create your-app-name-backend
# Deploy to Heroku
git push heroku main
Enter fullscreen mode Exit fullscreen mode

Explanation:

Git is a version control system that Heroku uses to deploy your code

Replace your-app-name-backend with a unique name for your backend

Frontend Deployment

  1. Create Production Build
cd frontend
npm run build
Enter fullscreen mode Exit fullscreen mode

Explanation: Creates optimized production files in the build folder.

  1. Install Serve Package
npm install -g serve
Enter fullscreen mode Exit fullscreen mode

Explanation: serve is a simple static file server for production builds.

  1. Create Procfile

Create a Procfile in the frontend directory with:

web: serve -s build
Enter fullscreen mode Exit fullscreen mode

Explanation: Tells Heroku to serve the built React application.

  1. Update API_BASE

Change the API_BASE in your React app to point to your deployed backend:

const API_BASE = "https://your-app-name-backend.herokuapp.com";
Enter fullscreen mode Exit fullscreen mode
  1. Deploy to Heroku
# Initialize git
git init
git add .
git commit -m "Initial commit"
# Create Heroku app
heroku create your-app-name-frontend
# Deploy
git push heroku main
Enter fullscreen mode Exit fullscreen mode

Replace your-app-name-frontend with a unique name for your frontend.

Alternative: Deploy with Netlify (Easier for Frontend)
For the frontend, Netlify is often easier than Heroku:

Build your React app: npm run build

Drag and drop the build folder to netlify.com

Update API_BASE to point to your deployed backend

Best Practices for Beginners

  • Code Organization: Keep your backend and frontend in separate directories
  • Environment Variables: Use environment variables for configuration (like API URLs)
  • Error Handling: Always add try-catch blocks around API calls
  • Code Comments: Comment your code to explain complex logic
  • Version Control: Use Git regularly to track changes
  • Incremental Development: Build and test one feature at a time
  • Troubleshooting Common Issues
  • CORS Errors: Make sure your backend allows your frontend domain
  • API Connection Issues: Check if your backend is running and the URL is correct
  • Database Problems: For production, consider using PostgreSQL instead of SQLite
  • Build Failures: Check that all dependencies are correctly listed

Next Steps
Now that you have a working CRUD application, you can:

  • Add user authentication
  • Implement more complex data relationships
  • Add image upload functionality
  • Improve the UI with a CSS framework like Bootstrap or Material-UI
  • Add form validation

Congratulations! You've built and deployed a full-stack web application. The skills you've learned form the foundation of modern web development. Keep experimenting and building—the best way to learn is by doing!

Remember, every developer started where you are now. With practice and persistence, you'll continue to grow your skills and build even more amazing applications. Happy coding!

Top comments (1)

Collapse
 
gilly7 profile image
Gilbert Ngeno

Learn how to deploy for free on render.com here in part 2