)
上一篇把 HTTP 的一来一回拆到了每一行——请求行与状态行、头、那个不能省的空行、体方法描述意图、状态码分清谁的错还用 curl -v 逐行验证过真实报文随后零依赖手搓出了 GET /api/profile也亲身体会到路由判断、状态行、响应头、404 兜底这些杂活写一个接口就要来一遍而它们属于 HTTP 规范本身谁写后端都躲不掉这一篇请出框架让 uvicorn 守端口、FastAPI 管接口把手搓的接口重写一遍再借 pydantic 的声明式校验补上 POST 接口从手搓到框架FastAPI 登场认识几个主流后端框架以及 FastAPI 和 uvicorn 各自负责什么然后把手搓的接口重写一遍再顺手加一个 POST那些杂活每个后端都一样手搓了一个 API也切身体会到它并不轻松为了把一段 JSON 发出去我们手写了路由判断、状态码、两行响应头、dumps、encode、404 兜底……而这才一个接口还只是 GET今天开始学一个后端框架——HTTP 的这些事儿用了框架会轻松很多框架这个词已经不陌生了讲 React 时说过框架就是管某一摊事的一套规则React 管的是 UI 组件 这一摊后端框架管的则是另一摊接住请求、解析内容、把响应发回去我们只需要按照框架的规则填上真正关注的部分——这个路径该返回什么数据认识几个主流后端框架Python 的后端框架不止一个Flask——老牌、轻量长期的入门经典生态成熟Django——大而全自带后台管理界面、用户系统还有一套操作数据库的 ORM 工具很多常用功能都有现成方案适合直接开发大型网站FastAPI——最年轻的一个专为写 API 而生样板代码极少、自动生成接口文档而且把字段和类型写清楚它就能自动帮我们解析和校验这里选 FastAPI主要有三个理由代码少、类型校验的反馈直接、自动生成接口文档——对一个以 API 为主、希望快速获得这些能力的 Python 新项目它是很合适的选择另外顺带推荐FastAPI 官网的 User Guide 写得特别好不光讲 是什么、怎么用还常常讲 为什么几乎可以当成一份手把手教程来读对中文的支持也不错文档里的 About 还专门对比了 Flask、Django 等框架值得一读——想深入学 FastAPI官网就是最好的教材和选 React 时的道理一样框架之间的概念是相通的把 FastAPI 用明白了回头看 Flask、看 Django都是熟面孔两个角色FastAPI 和 uvicorn手搓版的 main.py 其实同时干了两类工作判断路径、组织响应——决定 收到这个请求后返回什么用 HTTPServer(...) 和 serve_forever() 守住 8000 端口一直等待请求用了 FastAPI 之后这两类工作交给两个工具分工手搓版的职责现在由谁负责if self.path ...、组织响应FastAPI——负责定义接口HTTPServer(...)、serve_forever()uvicorn——负责运行服务器、监听端口所以FastAPI 负责 接口该做什么uvicorn 负责 让接口跑起来——FastAPI 自己不会守着端口等请求uvicorn 收到请求后会把它交给 FastAPI 处理一会儿我们先亲手指挥一次 uvicorn把它的命令认清楚再换官网的快捷命令 fastapi dev——这样以后在别处教程或报错里遇到 uvicorn 这个名字就都不陌生了把 FastAPI 装进来FastAPI 是第三方包装第三方包 这套动作之前已经完整走过一遍了那次装的是 requests——pip 装、落进 .venv这次只是换了包名安装命令用官网教程的同款先确认自己在 (zero-to-tech) 环境里装包前先看提示符老规矩cd ~/zero-to-tech/backend source .venv/bin/activate # 若提示符没带括号 pip install fastapi[standard]包名后面的方括号是 pip 的 套餐 写法fastapi[standard] FastAPI 本体 官方推荐的一套标准配件装完 pip list 看一眼列表一长串——刚认识的 uvicorn 就在里面它就是随这个套餐装进来的还有 starlette、pydantic、fastapi-cli 等一串没点名的包依赖还有依赖装 requests 时也见过这现象用 FastAPI 重写 /api/profile铺垫结束正主登场先把手搓版留作纪念mv main.py handmade.py新建 main.py写 FastAPI 版from fastapi import FastAPI app FastAPI() profile { heroTitle: 关于我, heroSubtitle: 项目创意灵感心得我的作品, } app.get(/api/profile) def get_profile(): return profile没了这就是全部app.get(/api/profile)这行是一个新面孔叫 装饰器——和手搓版里的 class 一样不用学会它读懂意思就行/api/profile 这个路径的 GET 请求交给下面这个函数处理拿它和 handmade.py 对一对上一次的杂活都去哪了手搓版亲手写的FastAPI 版if self.path ... 路由判断app.get(...) 一行装饰器send_response(200)自动send_header(Content-Type, ...)自动设置 JSON 响应的类型json.dumps(...).encode(...)自动把返回的 Python 字典序列化为 JSONelse 兜底 404自动没定义的路径自动回 404跑起来先用 uvicorn手动挡启动之前先确认手搓服务已经 Ctrl C 停掉了——一个端口同一时间只能由一个程序守着还记得端口吗要是忘了停下面这条命令会报 Address already in use地址已被占用——认得这个报错以后一见到就知道是端口被别的程序占着呢先直接指挥 uvicornuvicorn main:app --reloadmain:app 可以拆开看main : app文件 变量意思是让 uvicorn 去找 main.py 文件里的 app这里不写 .py--reload 是改代码自动重启——前端早就享受过这待遇npm run dev 改代码即时生效这是后端的同款开发时开着它就不用像手搓时那样每改一次手动 CtrlC 重启了服务跑起来了——uvicorn 守着 8000 端口把收到的请求交给 main.py 里的 app两个角色就这样接上了头换官网的快捷命令fastapi devCtrl C 停掉再用官网教程的写法跑一遍——注意这次连文件名都不用写fastapi dev启动输出里有几行值得认一认FastAPI Starting development server server Server started at http://127.0.0.1:8000 server Documentation at http://127.0.0.1:8000/docs tip Running in development mode, for production use: fastapi run INFO Uvicorn running on http://127.0.0.1:8000 (Press CTRLC to quit)最后一行Uvicorn running——刚才亲手指挥过的 uvicorn就在这fastapi dev 干的事就是替我们把 uvicorn 跑起来dev 是开发模式等于帮我们带上了 --reload连文件名都不用写——它默认就去找 main.py连 main:app 这种写法都省了tip 那行说正式上线用 fastapi run不带自动重启——到部署的时候会再见到它Documentation 那行先卖个关子一会儿揭晓两条命令都能用干的是同一件事以后统一用 fastapi dev 这种写法——更简洁也和官网文档一致在别处教程里见到 uvicorn main:app --reload认得出它是 手动挡 就行验证新开一个终端curl http://localhost:8000/api/profile和手搓版一模一样的 JSON接着再故意访问一个不存在的路径curl http://localhost:8000/nope回来的是 {detail:Not Found}——404 我们一行都没写而且它连 404 都带着 JSON 格式的说明比我们手搓的空 404 还周到目录里多了一个东西接口跑通之后回到 backend 目录看一眼ls会发现多出来一个目录pycache/再看看里面lspycache——是 Python 运行代码时自动生成的缓存文件能让 Python 下次加载代码时更快一点删掉也没关系运行代码时还会重新生成既然它不是我们亲手写的源码、随时可以重新生成就和 .venv、node_modules 一样不应该进 Git在项目的 .gitignore 里加上__pycache__/ *.py[cod]第一行忽略所有层级的pycache目录第二行忽略项目各处的 .pyc、.pyo、.pyd 这几类 Python 生成文件惊喜我们的 API 自己长出了文档浏览器打开http://localhost:8000/docs一个接口文档页面列着我们的 /api/profile展开它依次点击 Try it out 和 Execute页面会真的调用一次接口——留意其中的 Request URL、Response body 和 Response code请求地址、响应内容、状态码都替我们摆好了回想一下我们是照着 DeepSeek 的文档学会调用它的 API 的——文档是 API 的说明书而现在我们的 API 的说明书是 FastAPI 自动替我们写的代码一改文档跟着变这就是 框架把通用的事全包了 的又一个例子如果 /docs 打开是一片空白多半是网络问题——这个文档页面的样式和脚本默认从公共 CDN 加载国内网络偶尔抽风刷新几次或换个网络通常就好接口本身不受任何影响加一个 POST 接口/api/analyze还差文字实验室要用的那个接口提交一段文字返回分析结果提交内容——认过脸的这就是 POST先把需求说清楚一来一回长什么样动手之前先把这一来一回的数据形状定下来——API 是一份约定写接口的第一步就是把约定说清楚调用方提交什么要分析的那段文字请求体只需要一个字段{ text: 今天的风很轻 }我们回什么分析结果具体一点文字实验室的结果卡上有四个位置——原文、拼音、分数、情感标签——约定的形状来自调用方的需要不过真正的分析拼音、情感分数要等第三方库来做今天先把接口的形状立起来原文照抄回去其余三个先写死占位{ text: 今天的风很轻, score: 0.5, label: 偏平静, pinyin: 先占位 }形状定了里面怎么算 以后随时可以换——这正是那句 API 是一份约定 的提供方视角约定不变实现随便换之前刻意没有手搓 POST 接口因为它要做的杂活更多自己读 Content-Length、自己收字节、自己解析 JSON、自己校验字段全不全……看看 FastAPI 怎么处理动手三步写完这个接口一共改三处先一口气敲完写完再回头讲每一步的意思第一步在 main.py 顶部的 import 区加一行from pydantic import BaseModel第二步在 profile 后面加一段请求体的声明class AnalyzeRequest(BaseModel): text: str第三步在文件末尾加上接口本身app.post(/api/analyze) def analyze(req: AnalyzeRequest): return { text: req.text, score: 0.5, label: 偏平静, pinyin: 先占位, }三步写完完整的 main.py 是这样核对一下from fastapi import FastAPI from pydantic import BaseModel app FastAPI() profile { heroTitle: 关于我, heroSubtitle: 项目创意灵感心得我的作品, } class AnalyzeRequest(BaseModel): text: str app.get(/api/profile) def get_profile(): return profile app.post(/api/analyze) def analyze(req: AnalyzeRequest): return { text: req.text, score: 0.5, label: 偏平静, pinyin: 先占位, }回头看这三步在干嘛第一步的 BaseModel 是新面孔——它来自 pydanticpip list 里见过的那个包FastAPI 的老搭档专门管数据的解析和校验BaseModel 是它提供的 数据模型 基类继承它就能声明 某一类数据长什么样第二步的 class AnalyzeRequest(BaseModel)正是用它把刚定好的请求形状写了下来这类请求的请求体里必须有一个 text 字段而且是字符串注意这不是在处理请求而是在声明请求应该长什么样第三步的接口app.post 和 app.get 是同一个思路/api/analyze 的 POST 请求交给下面这个函数关键在参数 req: AnalyzeRequest——FastAPI 一看到这份声明就自动完成了手搓时代最狼狈的全部动作收字节、解析 JSON、校验字段、转成好用的对象所以函数里直接 req.text 就能拿到调用方提交的文字返回的正是定好的响应形状原文加三个写死的占位值我们写的声明FastAPI 自动完成text要求请求体里有这个字段: str要求这个字段是字符串req: AnalyzeRequest从 JSON 请求体解析出一个好用的对象如果字段缺失或类型不对返回校验错误那份声明的威力马上见分晓保存开发模式已自动重启测试curl http://localhost:8000/api/analyze \ -H Content-Type: application/json \ -d {text: 今天的风很轻适合把想法写下来}回来的正是定好的形状原文加三个写死的占位值这条命令眼熟吗-H Content-Type: application/json、-d {...}——方法我们压根没写是 -d 自动把它发成了 POST讲过的规矩和我们用 curl 调 DeepSeek 那条形状一模一样——那时我们是调用方看不见对面现在我们自己就是 对面再故意发一个错的——字段名写错这次加上 -i让 curl 把响应头和状态码也显示出来curl -i http://localhost:8000/api/analyze \ -H Content-Type: application/json \ -d {txt: 字段名写错了}先看第一行HTTP/1.1 422 Unprocessable Entity下面的 JSON 里明明白白指出缺 text 字段——校验代码我们一行没写再回到 /docs 刷新一下/api/analyze 已经自动出现了展开后还能看到请求体必须有一个字符串类型的 text——代码里的声明不但带来了自动校验也自动变成了文档再强调一次score、label、pinyin 现在都是写死的占位值这个接口今天只负责把 形状 立住真正的拼音和情感分数后面会用第三方库换成真的——到时还会亲眼看到 API 的一个好处内部实现整个换掉接口不变调用方毫无感觉最后一件事报错了怎么看后端阶段的最后一项生存技能我们故意制造一个 bug——把 analyze 里的 req.text 少写一个字母改成 req.txttext: req.txt, # 故意写错保存再用刚才那份正确的请求调用一次同样加上 -icurl -i http://localhost:8000/api/analyze \ -H Content-Type: application/json \ -d {text: 测试}第一行是 HTTP/1.1 500 Internal Server Error5xx服务方的问题——这次真的是我们的问题有意思的是刚才调用方把字段名写错成 txt得到的是 422现在同一个手误发生在我们自己的代码里变成了 500——谁的错状态码分得清清楚楚再看服务端终端这一次请求失败了但服务进程并没有退出修好之后还能继续接收请求终端里打出了一大段红字这就是 traceback错误回溯读终端报错有固定套路先看最后一行AttributeError: AnalyzeRequest object has no attribute txt——错误类型和原因一句话AnalyzeRequest 身上没有 txt 这个东西再往上找自己的文件File .../main.py, line XX——这里会显示实际出错的文件和行号每个人代码里的空行可能不同所以看到的数字不一定一样两步定位改回 req.text保存恢复正常报错不是事故是线索——最后一行说 是什么错上面几行说 在哪儿实在读不懂整段复制丢给 AI它读 traceback 比谁都熟练从今往后见到红字先别关终端先看最后一行收尾把依赖清单更新好现在两个接口都写完、依赖也确定了最后把装的东西记到 requirements.txt 的账上之前的 requirements.txt 里原本只有 requests重新生成一次pip freeze requirements.txt cat requirements.txtfastapi、uvicorn连同 starlette、pydantic 等一整套都进清单了——[standard] 套餐记的账比那份只有 requests 的长了一大截.venv 和 pycache 不进 Git但 requirements.txt 要跟着代码一起进 Git——别人才能照着它重建出同样的环境