Connect React to a Flask REST API
Build a fullstack Book Tracker app with a React frontend and Flask backend.
Introduction
30 Second Summary
Every web app you use has two halves: a frontend you interact with and a backend storing your data. When you click a button or submit a form, your browser sends a request to a server behind the scenes.
In this project, you will build a Book Tracker app with a React frontend and a Flask backend that communicate through HTTP requests and JSON responses.
What You'll Build
A fullstack Book Tracker app where React displays, adds, and deletes books by communicating with a Flask REST API.
By the end of this project, you'll have:
- A Flask REST API with GET, POST, and DELETE endpoints serving book data as JSON.
- A React frontend scaffolded with Vite that fetches and displays data from your API.
- A working fullstack CRUD application where you can add and delete books through the browser.
- Secret Mission: Add loading states, error handling, and CORS configuration for production readiness.
Are there any prerequisites?
This project assumes basic React knowledge (components, state, useEffect) and basic Flask knowledge (routes, returning JSON). You should also be comfortable creating Python virtual environments.
Before We Start
Before We Start...
Set Up Both Projects
Every fullstack app needs two parts that run independently: a backend that serves data, and a frontend that displays it.
In this step, you will create both projects side by side inside a single book-tracker folder. The backend is a Flask app. The frontend is a React app scaffolded with Vite.
In this step, get ready to:
- Create the Flask backend with a Python virtual environment.
- Scaffold the React frontend with Vite.
- Verify both projects are set up correctly.
Create the project folder and Flask backend
Your backend and frontend will live in separate folders under one parent directory. This keeps both codebases organized and independent.
- In VS Code, open a terminal by pressing Ctrl+` (backtick key).
- Move to your Desktop and create the project folders by running these commands:
cd ~/Desktop
mkdir book-tracker
cd book-tracker
mkdir backend
cd backend
- Confirm you are inside the backend folder by running:
ls
You should see nothing listed yet (the backend folder is empty). That is expected.
A virtual environment isolates this project's Python packages from everything else on your machine. It prevents version conflicts between projects.
- Create and activate a virtual environment by running these commands:
macOS
python3 -m venv venv
source venv/bin/activate
Windows
python -m venv venv
.\venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran the activation command (the second line). On Windows, if you see an error about execution policies, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned first, then try activating again.
help me activate my Python virtual environment
- Install Flask by running:
pip install Flask==3.1.3
You should see output ending with Successfully installed followed by Flask and its dependencies.
Seeing a pip error?
If you see a permissions error, make sure your virtual environment is activated (look for (venv) in your prompt). If pip itself is not found, try pip3 instead.
help me install Flask in my virtual environment
- Create an empty api.py file by running:
macOS
touch api.py
Windows
New-Item api.py
- Confirm the file was created by running:
ls
You should see api.py and the venv folder listed in the output.
Scaffold the React frontend
Vite is a build tool that scaffolds a ready-to-run React project in seconds. It also provides the development server you will use to preview your frontend.
- Navigate back to the book-tracker root folder by running:
cd ..
- Scaffold the React frontend by running:
npm create vite@latest frontend -- --template react
What does this command do?
- npm create vite@latest runs the Vite project scaffolder.
- frontend is the name of the folder it creates.
- -- --template react tells Vite to use the React template with JavaScript (not TypeScript).
Seeing a prompt about packages?
If the CLI asks you to confirm installing create-vite, type y and press Enter. If it asks to confirm the package name, press Enter to accept the default.
help me scaffold a Vite React project
- Move into the new frontend folder and install its dependencies by running:
cd frontend
npm install
You should see output listing added packages with no errors.
- Start the Vite development server by running:
npm run dev
- Open your browser and visit http://localhost:5173.
You should see a page with the Vite and React logos, along with a counter button. This confirms the frontend is working.
- Return to your terminal and press Ctrl+C to stop the development server.
Don't see the page at localhost:5173?
Check that npm run dev is still running in your terminal. Look for a line showing Local: http://localhost:5173/ in the output. If you see a different port number, use that URL instead.
help me get the Vite dev server running
Verify the folder structure
Both projects are now created. Let's open the full project in VS Code so you can see everything in one place.
- In VS Code, click File in the top menu bar.
- Click Open Folder.
- Navigate to your Desktop and select the book-tracker folder, then click Open.
In the Explorer sidebar on the left, you should see two top-level folders:
- backend/ containing api.py and the venv folder.
- frontend/ containing src/App.jsx, src/main.jsx, vite.config.js, and package.json.
Why two separate folders?
Keeping backend and frontend in separate folders mirrors how production apps are organized. Each project has its own dependencies, its own package manager, and can be deployed independently.
Your project skeleton is ready. Next up, you will write the Flask API that serves book data as JSON.
Build the Flask REST API
Your project structure is ready with a Flask backend and React frontend sitting side by side. Now it's time to give the backend something to say.
In this step, you'll build a REST API that serves book data as JSON. React will eventually consume these endpoints, but first you need to make sure Flask can handle GET, POST, and DELETE requests on its own.
In this step, get ready to:
- Write a Flask API with three endpoints (GET, POST, DELETE).
- Start the Flask development server.
- Verify the API returns properly formatted JSON.
Write the Flask API in api.py
A REST API exposes endpoints that respond to HTTP requests. You'll create three endpoints: one to list all books, one to add a book, and one to delete a book.
- In VS Code, open backend/api.py from the Explorer sidebar.
- Add the following code to set up the Flask app, sample data, and the GET endpoint:
from flask import Flask, request, jsonify
# Create the Flask application
app = Flask(__name__)
# In-memory storage for books (no database needed for learning)
books = [
{"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
{"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee"},
]
# Track the next available ID for new books
next_id = 3
# Return all books as a JSON array
@app.route("/api/books", methods=["GET"])
def get_books():
return jsonify(books)
What does this code do?
This sets up the foundation of your API.
- books is a Python list that acts as your database. Each book is a dictionary with id, title, and author.
- next_id keeps track of what ID to assign the next new book.
- The GET endpoint at /api/books returns the entire books list as JSON when someone visits that URL.
You won't see a change yet. The next chunk adds the ability to create books.
- Below the get_books function, add the POST endpoint:
# Add a new book from the request body
@app.route("/api/books", methods=["POST"])
def add_book():
global next_id
# Parse the JSON data sent from the frontend
data = request.get_json()
# Build a new book object with a unique ID
book = {"id": next_id, "title": data["title"], "author": data["author"]}
next_id += 1
books.append(book)
# Return the new book with 201 Created status
return jsonify(book), 201
What does this code do?
This endpoint handles POST requests to /api/books. When React sends a new book's title and author, Flask reads that JSON body with request.get_json(), builds a book dictionary, and appends it to the list.
The 201 status code means "Created" and tells the frontend the operation succeeded.
You won't see a change yet. The next chunk adds the delete capability and makes the server runnable.
- Below the add_book function, add the DELETE endpoint and the main block:
# Remove a book by its ID
@app.route("/api/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
global books
# Keep only books whose ID does not match the one being deleted
books = [b for b in books if b["id"] != book_id]
return jsonify({"message": "Book deleted"}), 200
# Run the Flask development server
if __name__ == "__main__":
app.run(debug=True)
What does this code do?
The DELETE endpoint accepts a book ID in the URL (like /api/books/1). It filters the list to keep every book except the one matching that ID.
The if __name__ == "__main__" block starts Flask's built-in development server when you run the file directly. The debug=True flag auto-restarts the server whenever you save changes.
- Save backend/api.py.
Seeing red squiggly lines in VS Code?
If VS Code shows import errors on flask, it may not be using the Python interpreter inside your virtual environment. Press Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows), type Python: Select Interpreter, and choose the one inside your backend/venv folder.
help me fix Python import errors in VS Code
✔️ Awesome, I've got everything!
Great. Double check you've saved backend/api.py before moving on.
ⓧ I'd like to double check the full code
from flask import Flask, request, jsonify
app = Flask(__name__)
books = [
{"id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald"},
{"id": 2, "title": "To Kill a Mockingbird", "author": "Harper Lee"},
]
next_id = 3
@app.route("/api/books", methods=["GET"])
def get_books():
return jsonify(books)
@app.route("/api/books", methods=["POST"])
def add_book():
global next_id
data = request.get_json()
book = {"id": next_id, "title": data["title"], "author": data["author"]}
next_id += 1
books.append(book)
return jsonify(book), 201
@app.route("/api/books/<int:book_id>", methods=["DELETE"])
def delete_book(book_id):
global books
books = [b for b in books if b["id"] != book_id]
return jsonify({"message": "Book deleted"}), 200
if __name__ == "__main__":
app.run(debug=True)
Run and test the API
With the code saved, it's time to start the Flask server and see your API in action.
- Open a terminal in VS Code by pressing Ctrl+` (backtick key, same on macOS and Windows).
- Navigate to the backend folder and activate your virtual environment by running:
macOS
cd backend
source venv/bin/activate
Windows
cd backend
.\venv\Scripts\activate
- Start the Flask development server by running:
python api.py
- You should see output showing Flask is running on http://127.0.0.1:5000.
- Open your browser and visit http://localhost:5000/api/books to see the JSON response.
Port 5000 already in use?
On macOS, the AirPlay Receiver service uses port 5000 by default. To fix this, go to System Settings > General > AirDrop & Handoff and turn off AirPlay Receiver. Then try running python api.py again.
If you see ModuleNotFoundError: No module named 'flask', your virtual environment is not activated. Re-run the activation command above and try again.
help me fix Flask not starting on my machine
Confirm the API returns proper JSON
When you visit http://localhost:5000/api/books in your browser, you should see a JSON array of two book objects.
What should I see?
The browser displays something like this:
[
{"author": "F. Scott Fitzgerald", "id": 1, "title": "The Great Gatsby"},
{"author": "Harper Lee", "id": 2, "title": "To Kill a Mockingbird"}
]
The key order may differ, but you should see both books with their id, title, and author fields. Flask's jsonify automatically sets the response Content-Type to application/json, so your browser knows to display it as structured data.
Why start with /api/ in the endpoint paths?
Prefixing every route with /api/ makes it easy to distinguish API requests from page requests later. When you configure the Vite proxy in the next step, you'll forward everything starting with /api to Flask and let Vite handle everything else.
- Leave the Flask server running in this terminal. You'll need it for the next step.
Your API is live and serving data. Next up, you'll connect your React frontend to this Flask backend using Vite's proxy.
Connect React to Flask with Vite Proxy
Your Flask API is serving book data on port 5000, and your React frontend is running on port 5173. Right now, these two servers have no idea the other exists.
In this step, you'll connect them using Vite's built-in proxy. The proxy intercepts API requests from React and forwards them to Flask, so your frontend can talk to the backend without any cross-origin issues.
In this step, get ready to:
- Configure Vite's proxy to forward API requests to Flask.
- Replace the default React component with one that fetches books from your API.
- Verify that React displays live data from Flask in the browser.
Configure the Vite proxy
When React calls fetch('/api/books'), the browser sends that request to the same server that served the page (Vite on port 5173). Without a proxy, Vite has no idea what /api/books means and returns a 404.
The proxy tells Vite: "any request starting with /api, forward it to Flask on port 5000." React never knows Flask exists. It just calls /api/books and gets data back.
- In VS Code, open frontend/vite.config.js.
- Replace the entire file contents with this code:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// Configure Vite with the React plugin and a proxy for API requests
export default defineConfig({
plugins: [react()],
server: {
proxy: {
// Forward any request starting with /api to the Flask server
'/api': 'http://localhost:5000'
}
}
})
What does this code do?
The server.proxy object tells Vite's dev server to intercept requests. Any request whose path starts with /api gets forwarded to http://localhost:5000 (your Flask server).
This means React can call fetch('/api/books') as if the API lives on the same server. Vite silently proxies it to Flask behind the scenes.
- Save the file.
Why use a proxy instead of calling Flask directly?
If React called http://localhost:5000/api/books directly, the browser would block it due to CORS (Cross-Origin Resource Sharing) restrictions. Browsers prevent JavaScript from making requests to a different origin (different port counts as different origin).
The proxy sidesteps this entirely. From the browser's perspective, every request goes to the same origin (port 5173). This is also how production reverse proxies (like Nginx) work.
Proxy not working later?
If the proxy doesn't take effect, make sure you restart the Vite dev server after saving this file. Vite needs to restart to pick up config changes.
Help me troubleshoot my Vite proxy configuration for forwarding /api requests to Flask.
Replace App.jsx with a component that fetches books
The default Vite template shows a counter demo. You'll replace it with a component that fetches books from your Flask API when the page loads.
- Open frontend/src/App.jsx.
- Select all the existing code and replace it with this:
import { useState, useEffect } from 'react'
import './App.css'
function App() {
// Store the list of books fetched from the API
const [books, setBooks] = useState([])
// Fetch books once when the component first renders
useEffect(() => {
fetchBooks()
}, [])
// Call the Flask API and store the response in state
async function fetchBooks() {
const response = await fetch('/api/books')
const data = await response.json()
setBooks(data)
}
return (
<div className="app">
<h1>Book Tracker</h1>
<ul className="book-list">
{books.map(book => (
<li key={book.id}>
<span>{book.title} by {book.author}</span>
</li>
))}
</ul>
</div>
)
}
export default App
What does this code do?
- useState([]) creates a books variable that starts as an empty array. When it updates, React re-renders the component.
- useEffect(() => { ... }, []) runs once when the component first appears on screen. The empty [] means "no dependencies, run only on mount."
- fetchBooks() calls fetch('/api/books'), which Vite proxies to Flask. The JSON response gets stored in state with setBooks(data).
- books.map() loops through the array and renders each book as a list item.
- Save the file.
Seeing a blank page?
Make sure you replaced all of the file contents, not just part. The old imports (like reactLogo and viteLogo) should be completely gone.
Help me fix my App.jsx so it fetches and displays books from my Flask API.
Now update the CSS files to style the book list. The default Vite styles won't match the new component.
- Open frontend/src/App.css.
- Select all the existing code and replace it with this:
/* Center the app and set a readable max width */
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
font-family: system-ui, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
/* Remove default list bullets and spacing */
.book-list {
list-style: none;
padding: 0;
}
/* Each book item gets a bottom border and spacing */
.book-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
What does this CSS do?
The .app class centers the content and caps it at 600px wide. The .book-list removes default bullet points, and each li uses flexbox to space the book title and any future buttons apart.
- Save App.css.
- Open frontend/src/index.css.
- Select all the existing code and replace it with this:
body {
margin: 0;
min-height: 100vh;
background: #fafafa;
}
- Save index.css.
Run both servers and verify the connection
Your Flask server is already running in Terminal 1. Now you need to start the Vite dev server so React can serve the frontend and proxy API requests to Flask.
- Open a second terminal in VS Code by pressing Cmd+Shift+` (macOS) or Ctrl+Shift+` (Windows).
- Navigate into the frontend folder and start the dev server by running these commands:
cd frontend
npm run dev
- Open your browser and navigate to http://localhost:5173.
You should see a "Book Tracker" heading with two books listed: "The Great Gatsby" and "To Kill a Mockingbird." This data is coming live from your Flask API.
What just happened?
React rendered, called fetch('/api/books'), and Vite intercepted it. Vite forwarded that request to Flask on port 5000. Flask returned the JSON array of books, Vite passed it back to React, and React stored it in state and re-rendered the list.
Not seeing any books?
Make sure your Flask server is still running in the other terminal. If it crashed or you closed it, switch to Terminal 1 and run python api.py again.
Check your browser's developer console (press F12) for errors. A 404 on /api/books means the proxy isn't configured correctly. Make sure you saved vite.config.js and restarted the Vite dev server.
Help me debug why my React app isn't displaying books from Flask.
✔️ Awesome, I've got everything!
Great. Double check you've saved all your files before moving on.
ⓧ I'd like to double check the full code
Here's the complete code for each file at this point.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': 'http://localhost:5000'
}
}
})
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [books, setBooks] = useState([])
useEffect(() => {
fetchBooks()
}, [])
async function fetchBooks() {
const response = await fetch('/api/books')
const data = await response.json()
setBooks(data)
}
return (
<div className="app">
<h1>Book Tracker</h1>
<ul className="book-list">
{books.map(book => (
<li key={book.id}>
<span>{book.title} by {book.author}</span>
</li>
))}
</ul>
</div>
)
}
export default App
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
font-family: system-ui, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
.book-list {
list-style: none;
padding: 0;
}
.book-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
body {
margin: 0;
min-height: 100vh;
background: #fafafa;
}
Your React frontend is now reading live data from Flask. Next up, you'll build a form that sends data the other direction. You'll add new books from the browser and watch them appear in the list.
Add Books from React
Your React app can now read books from Flask and display them in the browser. The next step is letting users create new books directly from the UI.
You'll build a form that captures a book's title and author, then sends that data to your Flask API as a POST request. When Flask responds with the newly created book, React adds it to the list instantly.
In this step, get ready to:
- Add state variables and a form to App.jsx.
- Write a function that sends POST requests to add books.
- Style and test the form in your browser.
Add form state variables to App.jsx
React forms work best with controlled inputs. Each input field is tied to a piece of state, so React always knows the current value.
- In frontend/src/App.jsx, find the line const [books, setBooks] = useState([]) and add these two new state variables directly below it:
const [title, setTitle] = useState('')
const [author, setAuthor] = useState('')
What are controlled inputs?
A controlled input stores its value in React state. Every keystroke updates state via onChange, and the input's value prop always reflects that state.
This gives you full control over form data when it's time to submit.
Write the handleAddBook function
When a user submits the form, you need to send the title and author to Flask as a POST request. Flask creates the book and returns it as JSON, which React then appends to the list.
- In App.jsx, find your fetchBooks function and add the following handleAddBook function directly below it:
async function handleAddBook(e) {
e.preventDefault()
const response = await fetch('/api/books', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, author })
})
const newBook = await response.json()
setBooks([...books, newBook])
setTitle('')
setAuthor('')
}
What does this function do?
- e.preventDefault() stops the browser from refreshing the page on form submit.
- fetch('/api/books', { method: 'POST', ... }) sends the book data to Flask. The Content-Type header tells Flask the body is JSON.
- JSON.stringify({ title, author }) converts the state values into a JSON string for the request body.
- After Flask responds, the function appends the new book to state and clears both inputs.
You won't see a change in the browser yet. The next chunk adds the form that calls this function.
- Now find the <ul className="book-list"> line inside your return statement and add this form block directly above it:
<form onSubmit={handleAddBook} className="add-form">
<input
type="text"
placeholder="Book title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<input
type="text"
placeholder="Author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
required
/>
<button type="submit">Add Book</button>
</form>
What does this form do?
- onSubmit={handleAddBook} fires your POST function when the form is submitted.
- Each <input> is bound to its matching state variable via value and onChange.
- The required attribute prevents the browser from submitting an empty form.
- Save App.jsx.
- Switch to your browser at http://localhost:5173. You should see two text inputs and an Add Book button above the book list.
Don't see the form?
- Check that the <form> block is placed above the <ul> and inside the <div className="app"> wrapper.
- Make sure both state variable lines are above the return statement, not inside it.
- Look at your terminal running Vite for any red error text pointing to a specific line number.
Help me debug why my form isn't appearing in React.
✔️ Awesome, I've got everything!
Great. Make sure App.jsx is saved before moving on.
ⓧ I'd like to double check the full code
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [books, setBooks] = useState([])
const [title, setTitle] = useState('')
const [author, setAuthor] = useState('')
useEffect(() => {
fetchBooks()
}, [])
async function fetchBooks() {
const response = await fetch('/api/books')
const data = await response.json()
setBooks(data)
}
async function handleAddBook(e) {
e.preventDefault()
const response = await fetch('/api/books', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, author })
})
const newBook = await response.json()
setBooks([...books, newBook])
setTitle('')
setAuthor('')
}
return (
<div className="app">
<h1>Book Tracker</h1>
<form onSubmit={handleAddBook} className="add-form">
<input
type="text"
placeholder="Book title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<input
type="text"
placeholder="Author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
required
/>
<button type="submit">Add Book</button>
</form>
<ul className="book-list">
{books.map(book => (
<li key={book.id}>
<span>{book.title} by {book.author}</span>
</li>
))}
</ul>
</div>
)
}
export default App
- Now open frontend/src/App.css and add these styles below the existing h1 rule to style the form:
.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.add-form input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.add-form button {
padding: 0.5rem 1rem;
background: #646cff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-form button:hover {
background: #535bf2;
}
What do these styles do?
- .add-form uses flexbox to lay the inputs and button side by side in a row.
- flex: 1 on the inputs makes them share available space equally.
- The button gets a purple background that darkens on hover for visual feedback.
- Save App.css and return to your browser.
- The form should now appear as a clean row with two input fields and a purple Add Book button.
Form still looks unstyled?
- Confirm that your <form> element has className="add-form" in App.jsx. The CSS targets this class name.
- Make sure the new CSS rules are placed outside any existing rule block, not accidentally nested inside the h1 selector.
Help me figure out why my CSS isn't applying to the form.
✔️ Awesome, I've got everything!
Great. Make sure App.css is saved before moving on.
ⓧ I'd like to double check the full code
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
font-family: system-ui, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.add-form input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.add-form button {
padding: 0.5rem 1rem;
background: #646cff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-form button:hover {
background: #535bf2;
}
.book-list {
list-style: none;
padding: 0;
}
.book-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
Test adding books
Both the form and the Flask API are ready. Time to confirm the full flow works end to end.
- Type 1984 in the title field and George Orwell in the author field.
- Click Add Book or press Enter.
- The new book should appear at the bottom of the list immediately. Both input fields should clear themselves.
- Refresh the page to confirm the book is still in the list. Flask stores it in memory as long as the server is running.
Why does the book persist after refresh?
The POST request actually sent the book to Flask, which stored it in the books list in memory. When React re-fetches on mount, Flask returns the updated list including your new book.
If you stop and restart the Flask server, the list resets to the two original books because the data lives in Python memory only.
Book not appearing after submit?
- Check that Flask is still running in Terminal 1. If the server stopped, your POST request has nowhere to go.
- Open your browser's DevTools (**F12**) and check the **Console** tab for errors. A 405 error means the request method doesn't match your Flask route.
- Verify that Content-Type is set to application/json in your handleAddBook fetch headers. Without it, Flask cannot parse the JSON body.
Help me debug why my book isn't being added.
Your app can now create new books from the browser and persist them on the server. Next up, you'll add the ability to delete books with a single click.
Delete Books from React
Your React app can now fetch and create books. The last piece of the CRUD puzzle is removing books you no longer want.
In this step, you'll write a function that sends a DELETE request to your Flask API and removes the book from React's state. You'll also wire up a button in each list item so the learner can trigger the delete with a single click.
In this step, get ready to:
- Write a delete function that calls the Flask API.
- Add a Delete button to each book in the list.
- Test the full delete flow end to end.
Write the handleDeleteBook function
When a user clicks Delete, your app needs to tell Flask to remove that specific book. The function will use the book's ID to target the correct API endpoint.
- In App.jsx, find the closing brace of the handleAddBook function.
- Add the following function directly below it:
async function handleDeleteBook(bookId) {
await fetch(`/api/books/${bookId}`, { method: 'DELETE' })
setBooks(books.filter(book => book.id !== bookId))
}
What does this code do?
This function takes a bookId and sends a DELETE request to /api/books/${bookId}. The Vite proxy forwards this to Flask, which removes the book from its in-memory list.
After the request completes, setBooks(books.filter(...)) creates a new array with the deleted book filtered out. React re-renders the list immediately without needing a full page refresh.
- You won't see a change yet. The next substep wires up a button that calls this function.
Add a delete button to each list item
Each book in the list needs a clickable button that triggers the delete function with that book's ID.
- In App.jsx, scroll down to the <ul className="book-list"> section.
- Find this code inside the .map():
<li key={book.id}>
<span>{book.title} by {book.author}</span>
</li>
- Add a Delete button inside the <li>, right after the <span> element, so it looks like this:
<li key={book.id}>
<span>{book.title} by {book.author}</span>
<button onClick={() => handleDeleteBook(book.id)}>Delete</button>
</li>
Why the arrow function in onClick?
Writing onClick={() => handleDeleteBook(book.id)} creates a new function that calls handleDeleteBook with the specific book's ID when clicked.
If you wrote onClick={handleDeleteBook(book.id)} without the arrow function, it would call the function immediately during rendering instead of waiting for the click.
- Save App.jsx.
- Switch to your browser at http://localhost:5173. You should see a Delete button next to each book in the list.
Don't see the Delete buttons?
Make sure the <button> element is inside the <li> tags, not outside the .map() block.
Check that both your Flask server (Terminal 1) and Vite dev server (Terminal 2) are still running.
help me figure out why my Delete buttons aren't showing up in the book list
The buttons work, but they look plain. Let's add some styling to make them stand out as destructive actions.
- Style the delete buttons by adding the following CSS at the end of App.css:
.book-list li button {
padding: 0.25rem 0.75rem;
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.book-list li button:hover {
background: #cc0000;
}
What does this CSS do?
The red background (#ff4444) signals a destructive action to the user. On hover, it darkens to #cc0000 to confirm the button is clickable.
The cursor: pointer property changes the mouse cursor to a hand icon on hover, which is standard for clickable elements.
- Save App.css and check your browser.
- The Delete buttons should now appear as small red buttons aligned to the right of each book.
Buttons not turning red?
Make sure the new CSS is placed after the existing .book-list li rule, not inside it. Each selector block needs its own curly braces.
Confirm that you saved App.css. Vite picks up CSS changes automatically, but only after a save.
help me figure out why my delete button styles aren't applying
Test the delete flow
Time to confirm that clicking Delete actually removes books from both the UI and the Flask server.
- Click the Delete button next to any book in your list.
- The book should disappear from the list immediately.
- Refresh the page to confirm the book stays deleted. Flask's in-memory list keeps the change as long as the server is running.
Why does the delete persist after refresh?
When you click Delete, the fetch request tells Flask to remove the book from its in-memory list. On refresh, React calls fetchBooks() again, which asks Flask for the current list. Since Flask already removed the book, it stays gone.
If you restart the Flask server, the books reset to the original two. That is because the data lives in memory, not a database.
✔️ Awesome, I've got everything!
Great. Double check you have saved both files.
ⓧ I'd like to double check the full code
Here is the complete App.jsx:
import { useState, useEffect } from 'react'
import './App.css'
function App() {
const [books, setBooks] = useState([])
const [title, setTitle] = useState('')
const [author, setAuthor] = useState('')
useEffect(() => {
fetchBooks()
}, [])
async function fetchBooks() {
const response = await fetch('/api/books')
const data = await response.json()
setBooks(data)
}
async function handleAddBook(e) {
e.preventDefault()
const response = await fetch('/api/books', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, author })
})
const newBook = await response.json()
setBooks([...books, newBook])
setTitle('')
setAuthor('')
}
async function handleDeleteBook(bookId) {
await fetch(`/api/books/${bookId}`, { method: 'DELETE' })
setBooks(books.filter(book => book.id !== bookId))
}
return (
<div className="app">
<h1>Book Tracker</h1>
<form onSubmit={handleAddBook} className="add-form">
<input
type="text"
placeholder="Book title"
value={title}
onChange={(e) => setTitle(e.target.value)}
required
/>
<input
type="text"
placeholder="Author"
value={author}
onChange={(e) => setAuthor(e.target.value)}
required
/>
<button type="submit">Add Book</button>
</form>
<ul className="book-list">
{books.map(book => (
<li key={book.id}>
<span>{book.title} by {book.author}</span>
<button onClick={() => handleDeleteBook(book.id)}>Delete</button>
</li>
))}
</ul>
</div>
)
}
export default App
Here is the complete App.css:
.app {
max-width: 600px;
margin: 0 auto;
padding: 2rem;
font-family: system-ui, sans-serif;
}
h1 {
text-align: center;
color: #333;
}
.add-form {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.add-form input {
flex: 1;
padding: 0.5rem;
border: 1px solid #ddd;
border-radius: 4px;
}
.add-form button {
padding: 0.5rem 1rem;
background: #646cff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.add-form button:hover {
background: #535bf2;
}
.book-list {
list-style: none;
padding: 0;
}
.book-list li {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border-bottom: 1px solid #eee;
}
.book-list li button {
padding: 0.25rem 0.75rem;
background: #ff4444;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.book-list li button:hover {
background: #cc0000;
}
Secret mission
Add Loading States, Error Handling, and CORS
Your Book Tracker works great on the happy path. But what happens when the server is down? Or a request fails? Or you deploy to production where the Vite proxy no longer exists? Time to make your app production-ready.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- A Python virtual environment in backend/ (no cost, local only).
- A Node.js project in frontend/ with installed dependencies (no cost, local only).
- Two running dev servers (Flask on port 5000, Vite on port 5173).
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with the Book Tracker app.
- Both dev servers remain active in your terminal windows.
- You can keep adding and deleting books while the servers run.
- The book-tracker folder stays on your Desktop for future reference.
Pause - I'll come back to this later
Shut down running processes to free up memory. Your project files stay on disk so you can pick up where you left off.
- In the terminal running Flask, press Ctrl+C to stop the server.
- In the terminal running Vite, press Ctrl+C to stop the dev server.
- Your book-tracker folder remains on your Desktop. To resume later, activate the virtual environment and start both servers again.
Delete - I don't want to use this again
Remove all project resources and start fresh. No cloud resources need to be cleaned up.
- In the terminal running Flask, press Ctrl+C to stop the server.
- In the terminal running Vite, press Ctrl+C to stop the dev server.
- Delete the entire project folder by running this command:
macOS
cd ~/Desktop
rm -rf book-tracker
Windows
cd ~/Desktop
rmdir /s /q book-tracker
- Confirm the folder is gone by running:
ls ~/Desktop
You should no longer see book-tracker in the output.
That's a Wrap!
That's a Wrap!
Nice work! You just built a fullstack Book Tracker application where React and Flask communicate seamlessly through HTTP requests and JSON responses.
You've learned how to:
- Built a Flask REST API with GET, POST, and DELETE endpoints that serve and accept JSON data.
- Connected a React frontend to Flask using Vite's proxy configuration and the browser's built-in fetch API to read, create, and delete records.
- Managed React state to keep the UI in sync with the server after every POST and DELETE request.
- Secret Mission: Added loading indicators, error handling around fetch calls, response validation with response.ok, and configured Flask-CORS for production-style cross-origin requests.
Ready to quiz yourself?