Developing a Single Page App with FastAPI and React
In this tutorial, you'll be building a CRUD app with FastAPI and React. We'll start by scaffolding a new React app with the Create React App CLI before building the backend RESTful API with FastAPI. Finally, we'll write them together and add the CRUD routes.
Final app:
Dependencies:
- React v16.13.1
- Create React App v3.4.1
- Node v12.1.0
- npm v6.14.0
- npx v6.14.8
- FastAPI v0.61.1
- Python v3.9
Before beginning this tutorial, you should be familiar with how React works. For a quick refresher on React, review the Main Concepts guide or the Intro to React tutorial.
Objectives
By the end of this tutorial, you will be able to:
- Develop a RESTful API with Python and FastAPI
- Scaffold a React project with Create React App
- Manage state operations with React Context API and Hooks
- Create and render React components in the browser
- Connect a React application to a FastAPI backend
What is FastAPI?
FastAPI is a Python web framework designed for building fast and efficient backend APIs. It handles both synchronous and asynchronous operations and has built-in support for data validation, authentication, and interactive API documentation powered by OpenAPI.
For more on FastAPI, review the following resources:
What is React?
React is a open-source, component-based JavaScript UI library that's used for building frontend applications.
For more, review the Getting Started guide from the official docs.
Setting up FastAPI
Start by creating a new folder to hold your project called "fastapi-react":
$ mkdir fastapi-react
$ cd fastapi-react
In the "fastapi-react" folder, create a new folder to house the backend:
$ mkdir backend
$ cd backend
Next, create and activate a virtual environment:
$ python3.9 -m venv venv $ source venv/bin/activate $ export PYTHONPATH=$PWD
Feel free to swap out virtualenv and Pip for Poetry or Pipenv.
Install FastAPI:
(venv)$ pip install fastapi==0.61.1 uvicorn==0.11.8
Uvicorn is an ASGI (Asynchronous Server Gateway Interface) compatible server that will be used for standing up the backend API.
Next, create the following files and folders in the "backend" folder:
└── backend ├── main.py └── app ├── __init__.py └── api.py
In the main.py file, define an entry point for running the application:
import uvicorn if __name__ == "__main__": uvicorn.run("app.api:app", host="0.0.0.0", port=8000, reload=True)
Here, we instructed the file to run a Uvicorn server on port 8000 and reload on every file change.
Before starting the server via the entry point file, create a base route in backend/app/api.py:
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI() origins = [ "http://localhost:3000", "localhost:3000" ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"] ) @app.get("/", tags=["root"]) async def read_root() -> dict: return {"message": "Welcome to your todo list."}
Why do we need CORSMiddleware? In order to make cross-origin requests -- e.g., requests that originate from a different protocol, IP address, domain name, or port -- you need to enable Cross Origin Resource Sharing (CORS). FastAPI's built-in CORSMiddleware
handles this for us.
The above configuration will allow cross-origin requests from our frontend domain and port which will run at localhost:3000
.
For more on the handling of CORS in FastAPI, review the official docs.
Run the entry point file from your console:
(venv)$ python main.py
Navigate to http://localhost:8000 in your browser. You should see:
{ "message": "Welcome to your todo list." }
Setting up React
Again, we'll be using the Create React App CLI tool to scaffold a new React application via npx.
Within a new terminal window, navigate to the project directory and then generate a new React application:
$ npx create-react-app frontend
$ cd frontend
If this is your first time scaffolding a React application using the Create React App tool, check out the documentation.
To simplify things, remove all files in the "src" folder except the index.js file. index.js is our base component.
Next, install a UI component library called Chakra UI:
$ npm install @chakra-ui/core @emotion/core @emotion/styled emotion-theming
After the installation, create a new folder called "components" in the "src" folder, which will be used to hold the application's components, along with two components, Header.jsx and Todos.jsx:
$ cd src $ mkdir components $ cd components $ touch {Header,Todos}.jsx
We'll start with the Header
component in the Header.jsx file:
import React from "react"; import { Heading, Flex, Divider } from "@chakra-ui/core"; const Header = () => { return ( <Flex as="nav" align="center" justify="space-between" wrap="wrap" padding="0.5rem" bg="gray.400" > <Flex align="center" mr={5}> <Heading as="h1" size="sm">Todos</Heading> <Divider /> </Flex> </Flex> ); }; export default Header;
After importing React and the Heading, Flex, and Divider components from Chakra UI, we defined a component to render a basic header. The component is then exported for use in the base component.
Next, let's rewrite the base component in index.js. Replace the previous code with:
import React from "react"; import { render } from 'react-dom'; import { ThemeProvider } from "@chakra-ui/core"; import Header from "./components/Header"; function App() { return ( <ThemeProvider> <Header /> </ThemeProvider> ) } const rootElement = document.getElementById("root") render(<App />, rootElement)
ThemeProvider, imported from the Chakra UI library, serves as the parent component for other components using Chakra UI. It provides a theme to all child components (Header
in this case) via React's Context API.
Start your React app from the terminal:
$ npm run start
This will open the React app in your default browser at http://localhost:3000. You should see:
What Are We Building?
For the remainder of this tutorial, you'll be building a todo CRUD app for creating, reading, updating, and deleting todos. By the end, your app will look like this:
GET Route
Backend
Start by adding a list of todos to backend/app/api.py:
todos = [ { "id": "1", "item": "Read a book." }, { "id": "2", "item": "Cycle around town." } ]
The list above is just dummy data used for this tutorial. The data simply represents the structure of individual todos. Feel free to wire up a database and store the todos there.
Then, add the route handler:
@app.get("/todo", tags=["todos"]) async def get_todos() -> dict: return { "data": todos }
Manually test the new route at http://localhost:8000/todo. Check out the interactive documentation at http://localhost:8000/docs as well:
Frontend
In the Todos.jsx component, start by importing React, the useState()
and useEffect()
hooks, and some Chakra UI components:
import React, { useEffect, useState } from "react"; import { Box, Button, Flex, Input, InputGroup, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Stack, Text, useDisclosure } from "@chakra-ui/core";
The useState
hook is responsible for managing our application's local state while the useEffect
hook allows us to perform operations such as data fetching.
For more on React Hooks, review the Primer on React Hooks tutorial and Introducing Hooks from the official docs.
Next, create a context for managing global state activities across all components:
const TodosContext = React.createContext({ todos: [], fetchTodos: () => {} })
In the code block above, we defined a context object via createContext that takes in two provider values: todos
and fetchTodos
. The fecthTodos
function will be defined in the next code block.
Want to learn more about managing state with the React Context API? Check out the React Context API: Managing State with Ease article.
Next, add the Todos
component:
export default function Todos() { const [todos, setTodos] = useState([]) const fetchTodos = async () => { const response = await fetch("http://localhost:8000/todo") const todos = await response.json() setTodos(todos.data) } }
Here, we created an empty state variable array, todos
, and a state method, setTodos
, so we can update the state variable. Next, we defined a function called fetchTodos
to retrieve todos from the backend asynchronously and update the todo
state variable at the end of the function.
Next, within the Todos
component, retrieve the todos using the fetchTodos
function and render the data by iterating through the todos state variable:
useEffect(() => { fetchTodos() }, []) return ( <TodosContext.Provider value={{todos, fetchTodos}}> <Stack spacing={5}> {todos.map((todo) => ( <b>{todo.item}</b> ))} </Stack> </TodosContext.Provider> )
Todos.jsx should now look like:
import React, { useEffect, useState } from "react"; import { Box, Button, Flex, Input, InputGroup, Modal, ModalBody, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalOverlay, Stack, Text, useDisclosure } from "@chakra-ui/core"; const TodosContext = React.createContext({ todos: [], fetchTodos: () => {} }) export default function Todos() { const [todos, setTodos] = useState([]) const fetchTodos = async () => { const response = await fetch("http://localhost:8000/todo") const todos = await response.json() setTodos(todos.data) } useEffect(() => { fetchTodos() }, []) return ( <TodosContext.Provider value={{todos, fetchTodos}}> <Stack spacing={5}> {todos.map((todo) => ( <b>{todo.item}</b> ))} </Stack> </TodosContext.Provider> ) }
Import the Todos
component in index.js file and render it:
import React from "react"; import { render } from 'react-dom'; import { ThemeProvider } from "@chakra-ui/core"; import Header from "./components/Header"; import Todos from "./components/Todos"; // new function App() { return ( <ThemeProvider> <Header /> <Todos /> {/* new */} </ThemeProvider> ) } const rootElement = document.getElementById("root") render(<App />, rootElement)
Your app at http://localhost:3000 should now look like this:
Try adding a new todo to the todos
list in backend/app/api.py. Refresh the browser. You should see the new todo. With that, we're done wth the GET request for retrieving all todos.
POST Route
Backend
Start by adding a new route handler to handle POST requests for adding a new todo to backend/app/api.py:
@app.post("/todo", tags=["todos"]) async def add_todo(todo: dict) -> dict: todos.append(todo) return { "data": { "Todo added." } }
With the backend running, you can test the POST route in a new terminal tab using curl
:
$ curl -X POST http://localhost:8000/todo -d \ '{"id": "3", "item": "Buy some testdriven courses."}' \ -H 'Content-Type: application/json'
You should see:
{ "data: [ "Todo added." ]" }
You should also see the new todo in the response from the http://localhost:8000/todo endpoint as well as at http://localhost:3000.
As an exercise, implement a check to prevent adding duplicate todo items.
Frontend
Start by adding the shell for adding a new todo to frontend/src/components/Todos.jsx:
function AddTodo() { const [item, setItem] = React.useState("") const {todos, fetchTodos} = React.useContext(TodosContext) }
Here, we created a new state variable that will hold the value from the form. We also retrieved the context values, todos
and fetchTodos
.
Next, add the functions for obtaining the input from the form and handling the form submission to AddTodo
:
const handleInput = event => { setItem(event.target.value) } const handleSubmit = (event) => { const newTodo = { "id": todos.length + 1, "item": item } fetch("http://localhost:8000/todo", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newTodo) }).then(fetchTodos) }
In the handleSubmit
function, we added a POST request and sent data to to the server with the todo info. We then called fetchTodos
to update todos
.
Just after the handleSubmit
function, return the form to be rendered:
return ( <form onSubmit={handleSubmit}> <InputGroup size="md"> <Input pr="4.5rem" type="text" placeholder="Add a todo item" aria-label="Add a todo item" onChange={handleInput} /> </InputGroup> </form> )
In the code block above, we set the form onSubmit
event listener to the handleSubmit
function that we created earlier. The todo item value is also updated as the input value changes via the onChange
listener.
The full AddTodo
component should now look like:
function AddTodo() { const [item, setItem] = React.useState("") const {todos, fetchTodos} = React.useContext(TodosContext) const handleInput = event => { setItem(event.target.value) } const handleSubmit = (event) => { const newTodo = { "id": todos.length + 1, "item": item } fetch("http://localhost:8000/todo", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(newTodo) }).then(fetchTodos) } return ( <form onSubmit={handleSubmit}> <InputGroup size="md"> <Input pr="4.5rem" type="text" placeholder="Add a todo item" aria-label="Add a todo item" onChange={handleInput} /> </InputGroup> </form> ) }
Next, add the AddTodo
component to the Todos
component like so:
export default function Todos() { const [todos, setTodos] = useState([]) const fetchTodos = async () => { const response = await fetch("http://localhost:8000/todo") const todos = await response.json() setTodos(todos.data) } useEffect(() => { fetchTodos() }, []) return ( <TodosContext.Provider value={{todos, fetchTodos}}> <AddTodo /> {/* new */} <Stack spacing={5}> {todos.map((todo) => ( <b>{todo.item}</b> ))} </Stack> </TodosContext.Provider> ) }
The frontend application should look like this:
Test the form by adding a todo:
PUT Route
Backend
Add an update route:
@app.put("/todo/{id}", tags=["todos"]) async def update_todo(id: int, body: dict) -> dict: for todo in todos: if int(todo["id"]) == id: todo["item"] = body["item"] return { "data": f"Todo with id {id} has been updated." } return { "data": f"Todo with id {id} not found." }
So, we checked for the todo with an ID matching the one supplied and then, if found, updated the todo's item with the value from the request body.
Frontend
Start by defining the component UpdateTodo
in frontend/src/components/Todos.jsx and passing two prop values, item
and id
to it:
function UpdateTodo({item, id}) { const {isOpen, onOpen, onClose} = useDisclosure() const [todo, setTodo] = useState(item) const {fetchTodos} = React.useContext(TodosContext) }
The state variables above are for the modal, which we will create shortly, and to hold the todo value to be updated. The fetchTodos
context value is also imported for updating todos
after the changes have been made.
Now, let's write the function responsible for sending PUT requests. In the UpdateTodo
component body, just after the state and context variables, add the following:
const updateTodo = async () => { await fetch(`http://localhost:8000/todo/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item: todo }) }) onClose() await fetchTodos() }
In the asynchronous function above, a PUT request is sent to the backend and then the onClose()
method is called to close the modal. fetchTodos()
is then invoked.
Next, render the modal:
return ( <> <Button h="1.5rem" size="sm" onClick={onOpen}>Update Todo</Button> <Modal isOpen={isOpen} onClose={onClose}> <ModalOverlay/> <ModalContent> <ModalHeader>Update Todo</ModalHeader> <ModalCloseButton/> <ModalBody> <InputGroup size="md"> <Input pr="4.5rem" type="text" placeholder="Add a todo item" aria-label="Add a todo item" value={todo} onChange={event => setTodo(event.target.value)} /> </InputGroup> </ModalBody> <ModalFooter> <Button h="1.5rem" size="sm" onClick={updateTodo}>Update Todo</Button> </ModalFooter> </ModalContent> </Modal> </> )
In the above code, we created a modal using Chakra UI's Modal components. In the modal body, we listened for changes to the textbox and updated the state object, todo
. Lastly, when the button "Update Todo" is clicked, the function updateTodo()
is invoked and our todo is updated.
The full component should now look like:
function UpdateTodo({item, id}) { const {isOpen, onOpen, onClose} = useDisclosure() const [todo, setTodo] = useState(item) const {fetchTodos} = React.useContext(TodosContext) const updateTodo = async () => { await fetch(`http://localhost:8000/todo/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ item: todo }) }) onClose() await fetchTodos() } return ( <> <Button h="1.5rem" size="sm" onClick={onOpen}>Update Todo</Button> <Modal isOpen={isOpen} onClose={onClose}> <ModalOverlay/> <ModalContent> <ModalHeader>Update Todo</ModalHeader> <ModalCloseButton/> <ModalBody> <InputGroup size="md"> <Input pr="4.5rem" type="text" placeholder="Add a todo item" aria-label="Add a todo item" value={todo} onChange={e => setTodo(e.target.value)} /> </InputGroup> </ModalBody> <ModalFooter> <Button h="1.5rem" size="sm" onClick={updateTodo}>Update Todo</Button> </ModalFooter> </ModalContent> </Modal> </> ) }
Before adding the component to the Todos
component, let's add a helper component for rendering todos to clean things up a bit:
function TodoHelper({item, id, fetchTodos}) { return ( <Box p={1} shadow="sm"> <Flex justify="space-between"> <Text mt={4} as="div"> {item} <Flex align="end"> <UpdateTodo item={item} id={id} fetchTodos={fetchTodos}/> </Flex> </Text> </Flex> </Box> ) }
In the component above, we rendered the todo passed to the component and attached an update button to it.
Replace the code in the return
block within the Todos
component:
return ( <TodosContext.Provider value={{todos, fetchTodos}}> <AddTodo /> <Stack spacing={5}> { todos.map((todo) => ( <TodoHelper item={todo.item} id={todo.id} fetchTodos={fetchTodos} /> )) } </Stack> </TodosContext.Provider> )
The browser should have a refreshed look:
Verify that it works:
DELETE Route
Backend
Finally, add the delete route:
@app.delete("/todo/{id}", tags=["todos"]) async def delete_todo(id: int) -> dict: for todo in todos: if int(todo["id"]) == id: todos.remove(todo) return { "data": f"Todo with id {id} has been removed." } return { "data": f"Todo with id {id} not found." }
Frontend
Let's write a component for deleting a todo, which will be used in the TodoHelper
component:
function DeleteTodo({id}) { const {fetchTodos} = React.useContext(TodosContext) const deleteTodo = async () => { await fetch(`http://localhost:8000/todo/${id}`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: { "id": id } }) await fetchTodos() } return ( <Button h="1.5rem" size="sm" onClick={deleteTodo}>Delete Todo</Button> ) }
Here, we started by invoking the fetchTodos
function from the global state object. Next, we created an asynchronous function that sends a DELETE request to the server and then updates the list of todos by, again, calling fetchTodos
. Lastly, we rendered a button that when clicked, triggers deleteTodo()
.
Next, add the DeleteTodo
component to the TodoHelper
:
function TodoHelper({item, id, fetchTodos}) { return ( <Box p={1} shadow="sm"> <Flex justify="space-between"> <Text mt={4} as="div"> {item} <Flex align="end"> <UpdateTodo item={item} id={id} fetchTodos={fetchTodos}/> <DeleteTodo id={id} fetchTodos={fetchTodos}/> {/* new */} </Flex> </Text> </Flex> </Box> ) }
The client application should be updated automatically:
Now, test the delete button:
Conclusion
This post covered the basics of setting up a CRUD application with FastAPI and React.
Check your understanding by reviewing the objectives from the beginning of this post. You can find the source code in the fastapi-react. Thanks for reading.
Looking for some challenges?
- Deploy the React app to Netlify using this guide and update the CORS object in the backend so that it's dynamically configured using an environment variable.
- Deploy the backend API server to Heroku ( feel free to host it on a platform of your choice) and replace the connection URL on the frontend. Again, use an environment variable for this. You can learn the basics of deploying FastAPI to Heroku from the Deploying and Hosting a Machine Learning Model with FastAPI and Heroku tutorial. For a beyond the basics look, check out the Test-Driven Development with FastAPI and Docker course.
- Set up unit and integration tests with pytest for the backend and React Testing Library for the frontend. The Test-Driven Development with FastAPI and Docker course covers how to test FastAPI with pytest while the Authentication with Flask, React, and Docker details how to test a React application with Jest and React Testing Library.
Comments