在本教程中,您将使用 FastAPI 和 React 构建一个 CRUD 应用程序。
在使用 FastAPI 构建后端 RESTful API 之前,我们将首先使用 Vite 搭建一个新的 React 应用程序。最后,我们将开发后端 CRUD 路由以及前端 React 组件。对于样式,我们将使用 Chakra UI,这是一个模块化组件库。
最终应用:
依赖:
- 反应 v19.0.0
- Vite v6.1.1 版本
- 节点 v22.13.1
- npm v10.9.2
- FastAPI 版本 0.115.7
- Python 版 3.13.1
在开始本教程之前,您应该熟悉 React 的工作原理。要快速复习 React,请查看快速入门指南中的主要概念,如果您是 React 的新手,请查看井字棋教程进行一些动手练习。
Contents
目标
在本教程结束时,您将能够:
- 使用 Python 和 FastAPI 开发 RESTful API
- 使用 Vite 搭建 React 项目的基架
- 使用 React Context API 和 Hook 管理 state作
- 在浏览器中创建和渲染 React 功能组件
- 将 React 应用程序连接到 FastAPI 后端
什么是 FastAPI?
FastAPI 是一个 Python Web 框架,旨在构建快速高效的后端 API。它处理同步和异步作,并内置了对数据验证、身份验证和由 OpenAPI 提供支持的交互式 API 文档的支持。
有关 FastAPI 的更多信息,请查看以下资源:
什么是 React?
React 是一个开源的、基于组件的 JavaScript UI 库,用于构建前端应用程序。
有关更多信息,请查看 React 思考教程,以了解 React 的工作原理。
设置 FastAPI
首先创建一个名为 “fastapi-react” 的新文件夹来保存你的项目:
$ mkdir fastapi-react
$ cd fastapi-react
在 “fastapi-react” 文件夹中,创建一个新文件夹来存放后端:
$ mkdir backend
$ cd backend
接下来,创建并激活虚拟环境:
$ python3.13 -m venv venv
$ source venv/bin/activate
$ export PYTHONPATH=$PWD
随意将 venv 和 Pip 换成 Poetry 或 Pipenv。有关更多信息,请查看现代 Python 环境。
安装 FastAPI:
(venv)$ pip install fastapi==0.115.7 uvicorn==0.34.0
Uvicorn 是一个与 ASGI(异步服务器网关接口)兼容的服务器,将用于建立后端 API。
接下来,在 “backend” 文件夹中创建以下文件和文件夹:
└── backend
├── main.py
└── app
├── __init__.py
└── api.py
在 main.py 文件中,定义用于运行应用程序的入口点:
import uvicorn
if __name__ == "__main__":
uvicorn.run("app.api:app", host="0.0.0.0", port=8000, reload=True)
在这里,我们指示文件在端口 8000 上运行 Uvicorn 服务器,并在每次文件更改时重新加载。
在通过入口点文件启动服务器之前,请在 backend/app/api.py 中创建一个 base 路由:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
origins = [
"http://localhost:5173",
"localhost:5173"
]
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."}
为什么我们需要 CORSMiddleware?为了发出跨域请求——即来自不同协议、IP 地址、域名或端口的请求——你需要启用跨域资源共享 (CORS)。FastAPI 的内置功能为我们处理了这个问题。CORSMiddleware
上述配置将允许来自我们的前端域和端口的跨域请求,这些请求将在 .localhost:5173
有关在 FastAPI 中处理 CORS 的更多信息,请查看官方文档。
从控制台运行入口点文件:
(venv)$ python main.py
在浏览器中导航到 http://localhost:8000。您应该会看到:
{
"message": "Welcome to your todo list."
}
设置 React
让我们用 Vite 搭建一个新的 React 应用程序。
打开一个新的终端窗口,导航到项目目录,然后生成一个新的 React 应用程序:
$ npm create [email protected]
回答以继续,并提供以下信息:y
Project name: frontend
Select a framework: › React
Select a variant: › TypeScript
安装依赖项:
$ cd frontend
$ npm install
接下来,安装一个名为 Chakra UI 的 UI 组件库:
$ npm install @chakra-ui/[email protected]
$ npm install @emotion/[email protected] @emotion/[email protected] [email protected]
安装后,在 “src” 文件夹中创建一个名为 “components” 的新文件夹,该文件夹将用于保存应用程序的组件,以及两个组件 Header.tsx 和 Todos.tsx:
$ cd src
$ mkdir components
$ cd components
$ touch {Header,Todos}.tsx
打开 frontend/src/index.css 并将其替换为以下代码:
:root{
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
[data-scope="dialog"] {
background: #1a1a1a !important;
--bg-currentcolor: #1a1a1a !important;
}
button {
background-color: #1a1a1a;
color: #ffffff;
}
input::placeholder {
color: #888888;
opacity: 1;
}
/* For Firefox */
input::-moz-placeholder {
color: #888888;
opacity: 1;
}
/* For Chrome/Safari/Opera */
input::-webkit-input-placeholder {
color: #888888;
}
/* For Internet Explorer */
input:-ms-input-placeholder {
color: #888888;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
现在让我们开始在 Header.tsx 文件中构建组件:Header
import React from "react";
import { Heading, Flex, Separator } from "@chakra-ui/react";
const Header = () => {
return (
Todos
);
};
export default Header;
从 Chakra UI 导入 React 和 Heading、Flex 和 Separator 组件后,我们定义了一个功能组件来渲染基本标头。然后导出该组件以在基本组件中使用。
接下来,让我们将 frontend/src/App.tsx 文件替换为以下代码:
import { ChakraProvider } from '@chakra-ui/react'
import { defaultSystem } from "@chakra-ui/react"
import Header from "./components/Header";
function App() {
return (
)
}
export default App;
从 Chakra UI 库导入的 ChakraProvider 作为其他使用 Chakra UI 的组件的父组件。它通过 React 的 Context API 为所有子组件(在本例中)提供主题。Header
请注意,我们从 Chakra UI 库导入了主题。您可以创建自己的自定义主题并将其传递给组件,要了解有关主题的更多信息,请查看官方 自定义文档。
defaultSystem
ChakraProvider
从终端启动您的 React 应用程序:
$ npm run dev
这将在 http://localhost:5173/ 的默认浏览器中打开 React 应用程序。您应该会看到:
我们在构建什么?
在本教程的其余部分,您将构建一个 todo CRUD 应用程序,用于创建、读取、更新和删除 todo。最后,您的应用程序将如下所示:
GET 路由
后端
首先将 todos 列表添加到 backend/app/api.py:
todos = [
{
"id": "1",
"item": "Read a book."
},
{
"id": "2",
"item": "Cycle around town."
}
]
上面的列表只是本教程中使用的虚拟数据。数据只是代表单个 todos 的结构。你可以随意连接一个数据库并将 todos 存储在那里。
然后,添加路由处理程序:
@app.get("/todo", tags=["todos"])
async def get_todos() -> dict:
return { "data": todos }
在 http://localhost:8000/todo 手动测试新路由。另请查看 http://localhost:8000/docs 上的交互式文档:
前端
在 Todos.tsx 组件中,首先导入 React、and 钩子和一些 Chakra UI 组件:useState()
useEffect()
import React, { useEffect, useState, createContext, useContext } from "react";
import {
Box,
Button,
Container,
Flex,
Input,
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
DialogTitle,
DialogTrigger,
Stack,
Text,
DialogActionTrigger,
} from "@chakra-ui/react";
钩子负责管理我们的客户端组件状态,而钩子允许我们在组件挂载到 DOM 时执行数据获取等作。useState
useEffect
有关 React Hook 的更多信息,请查看官方文档中的 React Hook 入门教程和内置 React Hooks。
现在,让我们定义我们的接口:Todo
interface Todo {
id: string;
item: string;
}
该接口用于定义将传递给组件的数据的形状。
Todo
Todos
接下来,创建一个上下文来管理所有组件中的全局状态活动:
const TodosContext = createContext({
todos: [], fetchTodos: () => {}
})
在上面的代码块中,我们通过 createContext 定义了一个上下文对象,该对象接受两个提供程序值:和 .该函数将在下一个代码块中定义。todos
fetchTodos
fetchTodos
想了解更多关于使用 React Context API 管理状态的信息吗?查看 React Context API:轻松管理状态 一文。
接下来,添加组件:Todos
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)
}
}
在这里,我们创建了一个空的状态变量数组 ,和一个状态方法 ,以便我们可以更新 state 变量。接下来,我们定义了一个函数,该函数用于从后端异步检索 todos,并在函数末尾更新 state 变量。todos
setTodos
fetchTodos
todo
接下来,在组件中,使用函数检索 todos 并通过迭代 todos 状态变量来渲染数据:Todos
fetchTodos
useEffect(() => {
fetchTodos()
}, [])
return (
{todos.map((todo: Todo) => (
{todo.item}
))}
)
Todos.tsx 现在应该如下所示:
import React, { useEffect, useState, createContext, useContext } from "react";
import {
Box,
Button,
Container,
Flex,
Input,
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogRoot,
DialogTitle,
DialogTrigger,
Stack,
Text,
DialogActionTrigger,
} from "@chakra-ui/react";
interface Todo {
id: string;
item: string;
}
const TodosContext = 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 (
{todos.map((todo: Todo) => (
{todo.item}
))}
)
}
在 App.tsx 文件中导入组件并渲染它:Todos
import { ChakraProvider } from '@chakra-ui/react'
import { defaultSystem } from "@chakra-ui/react"
import Header from "./components/Header";
import Todos from "./components/Todos"; // new
function App() {
return (
{/* new */}
)
}
export default App;
http://localhost:5173 上的应用程序现在应如下所示:
尝试将一个新的 todo 添加到 backend/app/api.py 中的列表中。刷新浏览器。你应该会看到新的 todo。这样,我们就完成了检索所有 todo 的 GET 请求。todos
POST 路由
后端
首先添加新的路由处理程序来处理将新 todo 添加到 backend/app/api.py 的 POST 请求:
@app.post("/todo", tags=["todos"])
async def add_todo(todo: dict) -> dict:
todos.append(todo)
return {
"data": { "Todo added." }
}
在后端运行的情况下,您可以使用以下方法在新的终端选项卡中测试 POST 路由:curl
$ curl -X POST http://localhost:8000/todo -d
'{"id": "3", "item": "Buy some testdriven courses."}'
-H 'Content-Type: application/json'
您应该会看到:
{
"data: [
"Todo added."
]"
}
您还应该在来自 http://localhost:8000/todo 终端节点的响应以及 http://localhost:5173 中看到新的 todo。
作为练习,实施检查以防止添加重复的 todo 项。
前端
首先添加 shell,用于将新 todo 添加到 frontend/src/components/Todos.tsx:
function AddTodo() {
const [item, setItem] = React.useState("")
const {todos, fetchTodos} = React.useContext(TodosContext)
}
在这里,我们创建了一个新的 state 变量,它将保存表单中的值。我们还检索了 context 值和 .todos
fetchTodos
接下来,将用于从表单获取输入并处理表单提交的函数添加到 :AddTodo
const handleInput = (event: React.ChangeEvent) => {
setItem(event.target.value)
}
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault()
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)
}
在该函数中,我们添加了一个 POST 请求,并将带有 todo 信息的数据发送到服务器。然后我们调用 update 。handleSubmit
fetchTodos
todos
React.ChangeEvent
是表示 input 元素的事件对象的类型。
紧跟在函数之后,返回要渲染的表单:handleSubmit
return (
)
在上面的代码块中,我们将表单事件侦听器设置为我们之前创建的函数。待办事项值也会随着输入值的变化而通过侦听器更新。onSubmit
handleSubmit
onChange
完整的组件现在应如下所示:AddTodo
function AddTodo() {
const [item, setItem] = React.useState("")
const {todos, fetchTodos} = React.useContext(TodosContext)
const handleInput = (event: React.ChangeEvent) => {
setItem(event.target.value)
}
const handleSubmit = (event: React.FormEvent) => {
event.preventDefault()
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 (
)
}
接下来,将组件添加到组件中,如下所示:AddTodo
Todos
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 (
{/* new */}
{todos.map((todo: Todo) => (
{todo.item}
))}
)
}
前端应用程序应如下所示:
通过添加 todo 来测试表单:
PUT 路由
后端
添加更新路由:
@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."
}
因此,我们检查了 ID 与提供的 ID 匹配的 todo,然后,如果找到,则使用请求正文中的值更新了 todo 的 item。
前端
首先在 frontend/src/components/Todos.tsx 中定义组件并传递两个 prop 值,然后传递给它:UpdateTodo
item
id
const UpdateTodo = ({ item, id, fetchTodos }: UpdateTodoProps) => {
const [todo, setTodo] = useState(item);
}
在文件顶部的 import 语句后,添加接口:UpdateTodoProps
interface UpdateTodoProps {
item: string;
id: string;
fetchTodos: () => void;
}
void
是一种表示缺少值的类型。我们用它来表示函数不返回值。
上面的 state 变量用于 modal,我们很快就会创建它,并保存要更新的 todo 值。上下文值也会被导入,以便在进行更改后进行更新。fetchTodos
todos
现在,让我们编写负责发送 PUT 请求的函数。在组件主体中,在 state 和 context 变量之后,添加以下内容:UpdateTodo
const updateTodo = async () => {
await fetch(http://localhost:8000/todo/${id}
, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ item: todo }),
});
await fetchTodos();
};
在上面的异步函数中,将 PUT 请求发送到后端,然后调用 。fetchTodos()
接下来,渲染模态框:
return (
Update Todo
setTodo(event.target.value)}
/>
)
在上面的代码中,我们使用 Chakra UI 的 Dialog 组件创建了一个模态。在模态体中,我们监听了文本框的更改并更新了状态对象 .最后,当单击“Update Todo”按钮时,将调用该函数并更新我们的 Todo。todo
updateTodo()
完整的组件现在应如下所示:
const UpdateTodo = ({ item, id, fetchTodos }: UpdateTodoProps) => {
const [todo, setTodo] = useState(item);
const updateTodo = async () => {
await fetch(http://localhost:8000/todo/${id}
, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ item: todo }),
});
await fetchTodos();
};
return (
Update Todo
setTodo(event.target.value)}
/>
)
}
在将组件添加到组件之前,让我们添加一个用于渲染 todos 的辅助组件来清理一下:Todos
function TodoHelper({item, id, fetchTodos}: TodoHelperProps) {
return (
{item}
)
}
将接口添加到文件顶部:TodoHelperProps
interface TodoHelperProps {
item: string;
id: string;
fetchTodos: () => void;
}
在上面的组件中,我们渲染了传递给组件的 todo,并为其附加了一个 update 按钮。
替换组件内块中的代码:return
Todos
return (
{
todos.map((todo) => (
))
}
)
浏览器应具有焕然一新的外观:
验证它是否有效:
DELETE 路由
后端
最后,添加 delete 路由:
@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."
}
前端
让我们编写一个用于删除 todo 的组件,它将在组件中使用:TodoHelper
const DeleteTodo = ({ id, fetchTodos }: DeleteTodoProps) => {
const deleteTodo = async () => {
await fetch(http://localhost:8000/todo/${id}
, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ id: id })
})
await fetchTodos()
}
return (
)
}
添加接口:DeleteTodoProps
interface DeleteTodoProps {
id: string;
fetchTodos: () => void;
}
在这里,我们首先从全局 state 对象调用函数。接下来,我们创建了一个异步函数,该函数向服务器发送 DELETE 请求,然后通过再次调用 .最后,我们呈现了一个按钮,单击该按钮时,会触发 .fetchTodos
fetchTodos
deleteTodo()
接下来,将组件添加到 :DeleteTodo
TodoHelper
function TodoHelper({item, id, fetchTodos}: TodoHelperProps) {
return (
{item}
{/* new */}
)
}
客户端应用程序应自动更新:
现在,测试删除按钮:
结论
本教程介绍了使用 FastAPI 和 React 设置 CRUD 应用程序的基础知识。
通过查看本教程开头的目标来检查您的理解情况。你可以在 fastapi-react 仓库中找到源码。感谢阅读。
寻找一些挑战?
- 使用本指南将 React 应用程序部署到 Netlify,并在后端更新 CORS 对象,以便使用环境变量动态配置它。
- 将后端 API 服务器部署到 Heroku 并替换前端上的连接 URL。同样,为此使用环境变量。您可以从使用 FastAPI 和 Heroku 部署和托管机器学习模型教程中学习将 FastAPI 部署到 Heroku 的基础知识。有关基础知识之外的内容,请查看使用 FastAPI 和 Docker 进行测试驱动开发课程。
- 使用后端的 pytest 和前端的 React 测试库设置单元测试和集成测试。使用 FastAPI 和 Docker 进行测试驱动开发课程介绍了如何使用 pytest 测试 FastAPI,而使用 Flask、React 和 Docker 进行身份验证则详细介绍了如何使用 Jest 和 React 测试库测试 React 应用程序。
//testdriven.io/courses/tdd-fastapi/)