冒险契约
53.65M · 2026-02-28
作为一名前端仔,一直想自己写点后端接口来进行一些功能的测试。结合前端 + 后端的视角,完整的走一遍整体的技术实现流程。既能巩固知识,又能融会贯通,达到真正搞懂的目的
比如 Token 鉴权,图片上传,大文件上传,Websocket 联调等。这些功能虽然做过很多次,可始终是浮于表面,只知道前端做了什么,至于传递参数调用接口之后的事情是一概不知。感觉前端只是一个切图仔 + API 调用侠,很难真正理解深层次的技术原理。所以我打算学点后端的东西,打通整个流程,真正的去理解它
我选择 Python 作为后端语言,UV 作为 Python 包和环境管理工具。想要学习 UV 入门教程请直接查阅我写的另一篇文章,本文不再赘述 UV 如何安装 Python 以及创建项目和虚拟环境等基础操作。
《Python 入门(一)- 用 UV 管理 Python》:juejin.cn/post/760585…
快来跟我一起学习吧!
FastAPI 官网:fastapi.tiangolo.com/zh/
pypi 上的地址:pypi.org/project/fas…
简介:FastAPI 是一个用于构建 API 的现代、快速(高性能)的 Web 框架,使用标准的 Python 语法,不需要学习新的语法、特定库的方法或类等等
FastAPI 使用了一种用于构建 Python Web 框架和服务器的标准,称为 ASGI。FastAPI 本质上是一个 ASGI Web 框架
Python 主流的 web 框架有 flask,Django 和 FastAPI,经过一番调研,最后我选择使用 FastAPI
有如下原因:
flask 太毛坯,Django 太重,并且两者的官网文档都太丑陋..(个人认为)
官网文档简洁美观
快速的开发 api 接口
自动生成交互式 API 文档(Swagger UI 和 ReDoc)
如何用 UV 创建 Python 项目就不演示了,创建项目后也不必特意创建和激活虚拟环境,UV 会在合适的时机智能的自动创建使用虚拟环境。关于 UV 的知识我已经在这篇文章内写的很清楚了: 《Python 入门(一)- 用 UV 管理 Python》:juejin.cn/post/760585…
在 Python 项目中安装 FastAPI
uv add "fastapi[standard]"
[standard] 在 Python 里被称为 “Extra”(额外选项),它告诉 UV 除了安装 FastAPI 核心库外,还要把标准可选依赖一起安装
UV 解析 fastapi 的 Extra (standard) 声明,发现其包含了 uvicorn、pydantic、httptools 等子依赖。详见官网介绍:fastapi.tiangolo.com/zh/#depende…
UV 会自动下载并利用硬链接将它们安装到项目的 .venv 中
并同步更新 pyproject.toml(声明依赖)
和 uv.lock(锁定精确版本)。
创建一个名为 main.py 的文件,其中包含 FastAPI 的导入以及创建 get 请求接口,完整内容如下:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int, q: str | None = None):
return {"item_id": item_id, "q": q}
uv run fastapi dev main.py
uv run:自动定为.venv 环境(自动寻找当前目录或上级目录的 pyproject.toml 文件所在目录中的 .venv),用这个虚拟环境运行命令
fastapi dev:FastAPI 官方 CLI,内部启动 uvicorn 服务器,默认 8000 端口,并自动开启 Reload(热部署) 模式(代码改了服务器自动重启)。默认运行在 ,并自动生成交互式文档
此时 fastapi 会自动生成两个交互式 API 文档,能在浏览器中直接调用和测试你的 API
Swagger UI:
点击 Try it out
点击 Execute
成功获取到接口返回内容
修改接口返回内容,将 World 改为闲云一鹤
再次点击 Execute,可以看到接口返回值同步更新了,说明热部署功能已开启,不需要重启服务就能自动更新内容(注意,如果更改了接口字段或者新增接口需要刷新文档页面)
ReDoc:
这是访问 redoc 文档
Python 是一个对于格式要求极其严格的语言,如果缩进或者一些格式错误,会报错导致无法运行
这里我故意在 app = FastAPI() 前面加上几个空格,产生了缩进错误,运行失败
在 VS Code 里安装 Python 扩展,它会自动帮你检查语法错误
不符合规范以及语法错误的地方,会标红提示
我们可以将配置再进一步优化,保存后让编辑器帮你自动格式化代码,就像前端的 eslint 和 prettier
强烈推荐 Ruff,它是一个用 rust 编写的,速度极快(比 Flake8 和 Black 快 10-100 倍)的 python 代码检查和格式化工具;并且 Ruff 和 uv 都是同一家公司(Astral)开发的
在你的项目终端运行,这会把 Ruff 记录在你的开发依赖中:
uv add --dev ruff
安装后会在 pyproject.toml 文件中添加 ruff 依赖
并且在 uv.lock 文件中锁定了 ruff 版本
在 VS Code 扩展市场搜索并安装 Ruff (作者是 Astral Software)。
{
// 设置 Python 文件的默认格式化程序为 Ruff
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
// 自动修复可修复的错误(比如多余的空格、没用的 import)
"source.fixAll.ruff": "explicit",
// 自动对 import 语句进行排序
"source.organizeImports.ruff": "explicit"
}
},
// 让 Ruff 插件优先使用你项目里 uv add 安装的那个版本
"ruff.importStrategy": "fromEnvironment"
}
建议用户区和工作区都配置,这样无论是你个人在电脑创建多个 Python 项目,还是团队合作让别人 git clone 你的项目时,都能确保拥有同样的格式化配置!
tip: 记得提醒同事装插件:配置是有了,但如果同事没装 VS Code 的 Ruff 插件,配置是不会生效的
下面测试保存代码自动格式化的配置是否生效
将下面的代码复制到 main.py 文件中,然后保存
import time, datetime
def hello():
x=1;
y=2;
print('hello world!',x + y)
这是格式化后的代码
def hello():
x = 1
y = 2
print("hello world!", x + y)
自动完成了下面的操作:
自动删除已导入却未使用的库:time, datetime
缩进从两个空格变成了四个空格
x和y赋值,等号前后去掉了空格,并且后面去掉了多余的分号
'hello world!' 单引号也变成了双引号,并且逗号后面加上了空格
__pycache__ 文件夹前面运行 uv run fastapi dev main.py 后项目根目录下新增了一个 __pycache__ 文件夹,并且里面新增了一个 main.cpython-313.pyc 文件
这是 Python 自己生成的“编译缓存”。运行 main.py 时,它会把代码编译成更快执行的字节码(.pyc 文件),然后存到 __pycache__ 文件夹里。
下次运行如果你没改代码,Python 跳过编译过程直接读这个缓存文件,启动速度会更快一点
千万不要将它们提交到仓库,因为这是临时文件、每台电脑都会自己生成,提交上去完全没用
在项目根目录创建一个 .gitignore 文件,写上 __pycache__
__pycache__ 文件可以随便删除,没有任何坏影响,只是下次启动时 Python 需要重新编译一下,启动时间会稍微长那么零点几秒
路由是 url 地址和处理函数之间的映射关系,它决定了当用户访问某个网址时,服务器应该执行哪段代码来返回结果
人话就是路由 = 装饰器 函数
用装饰器装饰函数,当请求路径的时候,执行路由对应的函数
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"Hello": "掘金-闲云一鹤"}
@app.get("/song")
async def get_song():
return {"门前大桥下": "游过一群鸭"}
上面代码中 app 是 FastApi 实例
get 是请求方法
/ 是请求路径
@app.get("/") 组成了装饰器
read_root() 是函数
return {"Hello": "掘金-闲云一鹤"} 是函数的响应结果
访问路由查看效果
输入不同的路由,返回不同的接口
什么是参数?
参数就是客户端发送请求时附带的额外信息和指令
参数的作用是让同一个接口能根据不同的输入参数,返回不同的输出结果,实现接口的动态交互
参数类型
参数分为: 路径参数,查询参数,请求体参数
路径参数出现在 Url 路径的后面,例如:/user/{name}
路径参数用于指向唯一的,特定的资源,方法为 GET
完整代码:
from fastapi import FastAPI
app = FastAPI()
@app.get("/users/{id}")
async def get_user(id: int, name: str | None = None):
return {"id": id, "name": name}
Request URL 示例:
http://127.0.0.1:8000/users/6?name=xyyh
Response body 示例:
{
"id": 6,
"name": "xyyh"
}
类型注解用于给参数添加额外的信息和校验,比如限制 ID 的范围值。
如果只限制参数的类型,就用 Python 的原生注解,比如 id: int 或者 name: str
如果有额外的限制,比如范围值和长度限制等,就需要用到 Path 注解,Path 函数需要先导入再使用
完整代码:
from fastapi import FastAPI, Path
app = FastAPI()
@app.get("/users/{id}")
async def get_user(
id: int = Path(..., gt=0, lt=101, description="用户ID,取值范围1-100"),
name: str | None = None,
):
return {"msg": f"这是一个用户接口, id为{id}, 姓名为{name}"}
其中 ... 表示必填,gt 表示值必须大于此值,lt 表示值必须小于此值
具体以及更多用法请查阅 FastAPI 官方文档:fastapi.tiangolo.com/zh/referenc…
Request URL 示例:
http://127.0.0.1:8000/users/6?name=xyyh
Response body 示例:
{
"msg": "这是一个用户接口, id为6, 姓名为xyyh"
}
我故意将 ID 输入为不符合范围内的值,它提醒我输入值应大于0
输入正常范围的 ID 值,接口正常返回
查询参数出现在 Url 路径的后面,紧跟一个问号,然后通过 key=value 形式展示参数,如果有多个参数中间用 & 符号进行拼接,例如:user?name=张三&age=18
查询参数用于对数据进行过滤,排序,分页等操作,方法为 GET
完整代码:
from fastapi import FastAPI
app = FastAPI()
# 查询用户列表 page: 当前页, size:每页条数
@app.get("/users/user_list")
async def get_user_list(page: int = 1, size: int = 10):
return {"msg": f"这是查询用户列表的接口, 当前为{page}页, 每页条数为{size}"}
= 号后面直接赋值,表示默认值
Request URL 示例:
http://127.0.0.1:8000/users/user_list?page=1&size=10
Response body 示例:
{
"msg": "这是查询用户列表的接口, 当前为1页, 每页条数为10"
}
与路径参数相同,查询参数也支持 python 原生注解
与路径参数不同(路径参数使用 Path 注解),查询参数需要使用 Query 来做类型注解,同样需要先引入再使用
完整代码:
from fastapi import FastAPI, Query
app = FastAPI()
# 查询用户列表 page: 当前页, size:每页条数
@app.get("/users/user_list")
async def get_user_list(
page: int = Query(1, ge=1, description="当前页码,必须大于等于1"),
size: int = Query(10, ge=1, le=100, description="每页条数必须在1-100之间"),
):
return {"msg": f"这是查询用户列表的接口, 当前为{page}页, 每页条数为{size}"}
Request URL 示例:
http://127.0.0.1:8000/users/user_list?page=1&size=10
Response body 示例:
{
"msg": "这是查询用户列表的接口, 当前为1页, 每页条数为10"
}
将页码和每页条数输入不符合范围内的值,也正常收到了提示
输入正常范围的值,接口正常返回
请求体参数出现在 http 请求的消息体(body)中,
请求体参数用于创建,更新资源,携带大量数据,如 json,方法为 POST,PUT 等
在 http 协议中,一个完整的请求由三部分组成:
完整代码:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
# 用户注册
class User(BaseModel):
username: str
password: str
@app.post("/users/register")
async def register_user(user: User):
# 在这里可以添加用户注册的逻辑,例如将用户信息保存到数据库
return {"msg": f"用户 {user.username} 注册成功!"}
Request URL 示例:
http://127.0.0.1:8000/users/register
Response body 示例:
{
"msg": "用户 掘金-闲云一鹤 注册成功!"
}
请求体参数可以用 python 原生注解或者 Field 注解
要先导入 Field 函数,用法跟 Path 和 Query 差不多
完整代码:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI()
# 用户注册
class User(BaseModel):
username: str = Field(default="掘金-闲云一鹤", description="用户名")
password: str = Field(
..., min_length=6, max_length=20, description="密码,长度必须在6到20之间"
)
@app.post("/users/register")
async def register_user(user: User):
# 在这里可以添加用户注册的逻辑,例如将用户信息保存到数据库
return {"msg": f"用户 {user.username} 注册成功!"}
密码长度必须在6到20之间,我输入三位数
提示我长度不够
输入六位数密码,正常提交接口
默认情况下,FastAPI 会将代码中的 Python 对象(字典,列表,pydantic 模型等),自动转换为 json 响应格式(JSONResponse)
FastAPI 内置的响应类型有:JSONResponse(json格式),HTMLResponse(HTML 内容),PlainTextResponse(纯文本),FileResponse(文件下载),StreamingResponse(流式响应),RedirectResponse(重定向)
如果想要接口返回其他的响应格式,需要手动设置,下面将详细介绍如何使用不同的响应
HTMLResponse 用于返回 HTML 内容
HTMLResponse 响应的 content-type 值为:text/html; charset=utf-8
完整代码:
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
@app.get("/", response_class=HTMLResponse)
async def get_html():
return """
<html>
<head>
<title>闲云一鹤</title>
</head>
<body>
<h1>这是一个简单的HTML响应</h1>
<p>我是掘金-闲云一鹤</p>
<p>欢迎访问我的掘金主页:>
</body>
</html>
"""
浏览器访问可查看渲染效果:
FileResponse 用于返回文件下载
FileResponse 响应的 content-type 值为:content-type: image/jpeg
完整代码:
from fastapi import FastAPI
from fastapi.responses import FileResponse
app = FastAPI()
@app.get("/file")
async def get_file():
file_path = "./img/壁纸.jpg"
return FileResponse(
file_path,
)
浏览器访问可查看图片:
可以理解为提前定义了接口的数据字段以及类型,如果接口返回的数据字段以及类型不符合提前定义的值,就会报错。前端仔表示感觉很像前端的 Typescript
完整代码:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Item(BaseModel):
name: str
description: str = None
price: float
tax: float = None
@app.post("/items/")
async def create_item(item: Item):
return item
FastAPI 通过 HTTPException 来处理异常
完整代码:
from fastapi import FastAPI, HTTPException
app = FastAPI()
@app.get("/users/{id}")
async def get_user(id: int):
# 这里模拟一个用户ID列表,实际应用中可以从数据库中查询
id_list = [1, 2, 3, 4, 5]
if id not in id_list:
raise HTTPException(status_code=404, detail="用户不存在")
return {
"id": id,
}
如果输入的 ID 值不在 id_list 中,就会抛出错误:
{ "detail": "用户不存在" }
中间件是一个在每次请求进入 FastAPI 应用时都会被执行的函数
中间件的作用是为每个请求添加统一的处理逻辑(记录日志,身份认证,跨域,设置响应头,性能坚控等)
它在请求到达实际的路径操作(路由处理函数)之前运行,并且在响应返回给客户端之前再运行一次
多个中间件的执行顺序是,从下到上
完整代码:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def read_root():
return {"Hello": "World"}
@app.middleware("http")
async def middleware1(request, call_next):
print("中间件-1: 开始处理请求.")
response = await call_next(request)
print("中间件-1: 请求处理完成.")
return response
@app.middleware("http")
async def middleware2(request, call_next):
print("中间件-2: 开始处理请求.")
response = await call_next(request)
print("中间件-2: 请求处理完成.")
return response
调用接口后,返回 json 数据
{
"Hello": "World"
}
并且 vscode 终端打印结果:
中间件-2: 开始处理请求.
中间件-1: 开始处理请求.
中间件-1: 请求处理完成.
中间件-2: 请求处理完成.
作用:使用依赖注入系统来共享通用逻辑,减少代码重复
依赖项:可重用的组件(函数/类),负责提供某种功能或数据
注入:FastAPI 自动帮你调用依赖项,并将结果“注入”到路径操作函数中
使用场景:处理请求参数,共享业务逻辑,共享数据库连接,安全和认证
完整代码:
from fastapi import Depends, FastAPI, Query
app = FastAPI()
# 依赖项:公共分页
async def common_page_and_size(
page: int = Query(1, ge=1, description="当前页码,必须大于等于1"),
size: int = Query(10, ge=1, le=100, description="每页条数必须在1-100之间"),
):
return {"msg": f"这是查询用户列表的接口, 当前为{page}页, 每页条数为{size}"}
@app.get("/users/user_list")
async def get_user_list(pagination: dict = Depends(common_page_and_size)):
return pagination
ORM(Object-RelationalMapping,对象关系映射) 是一种编程技术,用于在面向对象编程语言和关系型数据库之间建立映射。它允许开发者通过操作对象的方式与数据库进行交互 ,而无需直接编写复杂的 SQL 语句
ORM 工具有:SQLALchemy ORM,Django ORM,Tortoise ORM
具体的操作以及如何连接数据库,放在下一篇文章来写吧,如果有人看的话