FastAPI 入门:给寒柳别苑搭一个小型 API
最近开始系统学习 FastAPI。与其把知识点停留在“会写一个 Hello World”,不如给一个具体的网站设计一组接口。
所以这篇学习记录选择 zylatent.com 作为例子,假设我们要为寒柳别苑提供一层轻量的 API:
- 返回站点基本信息;
- 查询文章列表和文章详情;
- 接收访客留言;
- 自动生成接口文档;
- 让前端能够明确知道请求格式和错误原因。
这里的域名不是抽象的占位符:这组学习接口已经部署在 api.zylatent.com,文章末尾的面板会直接调用它。真正复杂的业务仍然需要数据库、认证和更严格的安全策略;这里先把最小的请求—响应链路跑通。
1. FastAPI 解决的是什么问题
如果不用框架,单纯用 Python 内置的 http.server 写一个接口,需要自己处理很多与业务无关的细节:
- 判断请求方法和路径;
- 读取请求体;
- 解析 JSON;
- 检查字段是否存在、类型是否正确;
- 设置状态码和响应头;
- 把 Python 对象序列化成 JSON;
- 为每个接口补充说明文档。
真正的业务可能只有一句话:访问 /api/site,返回站点信息。
FastAPI 的价值就在于把这些通用工作收拢起来,让我们用 Python 类型标注和装饰器描述接口。它建立在 Starlette 和 Pydantic 之上:前者负责 Web 层能力,后者负责数据模型与校验。
FastAPI 不是“完全不需要理解 HTTP”的魔法。路由、方法、状态码、请求体和响应体仍然存在,只是框架把重复的胶水代码替我们组织好了。
2. 创建项目和运行环境
FastAPI 官方目前推荐使用 uv 管理项目和虚拟环境。使用 pip 和 venv 也可以,关键是不要把项目依赖直接装进系统 Python。
使用 uv
uv init fastapi-zylatent --barecd fastapi-zylatentuv add "fastapi[standard]"fastapi[standard] 会安装 FastAPI 的标准依赖,其中包括运行服务的 Uvicorn 和提供 fastapi 命令的 CLI。
使用 venv 和 pip
python -m venv .venv
# Windows PowerShell.\.venv\Scripts\Activate.ps1
# macOS / Linuxsource .venv/bin/activate
pip install "fastapi[standard]"如果你已经在使用 Conda,也可以把环境隔离交给 Conda,只要保证 FastAPI 和项目其他依赖安装在同一个环境中即可。
3. 第一个接口:让服务先跑起来
在项目根目录新建 main.py:
from fastapi import FastAPI
app = FastAPI( title="寒柳别苑 API", description="为 zylatent.com 提供的学习型接口", version="0.1.0",)
@app.get("/")def read_root(): return {"message": "寒柳别苑 API 正在运行"}启动开发服务器:
# 使用 uvuv run fastapi dev
# 如果已经激活虚拟环境,也可以直接运行fastapi dev本地开发时,服务默认监听 http://127.0.0.1:8000。浏览器访问根路径,可以看到:
{ "message": "寒柳别苑 API 正在运行"}这篇文章对应的学习 API 已经部署在 api.zylatent.com。线上访问使用 HTTPS 的 443 端口,外部请求由 Nginx 转发到服务器内部的 FastAPI 8000 端口;读者不需要直接访问或暴露内部端口,可以直接运行:
curl.exe https://api.zylatent.com/healthcurl.exe https://api.zylatent.com/api/site如果你看到 {"status":"ok"},说明请求已经从命令行经过域名、HTTPS 和反向代理抵达 FastAPI。
这里的 @app.get("/") 是一个路径操作声明:当请求方法是 GET、路径是 / 时,调用下面的 read_root 函数。
4. /docs:接口文档不是额外工作
线上体验可以直接打开:
https://api.zylatent.com/docs如果你是在自己的电脑上启动开发服务,再使用本地地址:
http://127.0.0.1:8000/docs可以看到 Swagger UI。它会根据代码里的路由、类型标注和 Pydantic 模型自动生成:
- 接口列表;
- 请求参数结构;
- 响应格式;
- 状态码说明;
- 可以直接执行的测试表单。
FastAPI 同时会生成 OpenAPI Schema。也就是说,文档不是额外维护的一份 Markdown,而是从接口代码中派生出来的描述。
对个人项目来说,/docs 很适合充当第一版内部后台:先让自己和前端能够看懂、调用 API,再决定是否需要单独制作一套文档页面。
5. 用一个站点接口理解响应模型
先定义站点资料模型。这里使用 Pydantic 的 BaseModel 作为响应模型:
from pydantic import BaseModel
class SiteProfile(BaseModel): name: str domain: str description: str topics: list[str]
SITE_PROFILE = SiteProfile( name="寒柳别苑", domain="zylatent.com", description="记录学习、工作与思考。", topics=["Python", "C++", "Claude Code Skills", "诗歌", "写作"],)
@app.get("/api/site", response_model=SiteProfile)def read_site_profile(): return SITE_PROFILE请求:
curl.exe https://api.zylatent.com/api/site响应:
{ "name": "寒柳别苑", "domain": "zylatent.com", "description": "记录学习、工作与思考。", "topics": ["Python", "C++", "Claude Code Skills", "诗歌", "写作"]}response_model 的作用不只是给编辑器看。它还会参与响应数据的校验和 OpenAPI 文档生成,帮助我们明确“接口允许返回什么”。
6. 路径参数和查询参数:查询文章列表
接下来把示例扩展成一个简化的文章 API。与其把文章摘要继续写死在 main.py,不如单独维护一个 JSON 文件:接口负责读取它,新增文章时只需要增加一条记录。
import jsonfrom pathlib import Path
from fastapi import Query
class PostSummary(BaseModel): slug: str title: str category: str tags: list[str]
class PostDetail(PostSummary): content: str
POSTS_FILE = Path(__file__).parent / "data" / "posts.json"
def load_posts() -> list[PostDetail]: with POSTS_FILE.open(encoding="utf-8") as file: return [PostDetail.model_validate(item) for item in json.load(file)]
POSTS = load_posts()
@app.get("/api/posts", response_model=list[PostSummary])def list_posts( category: str | None = None, limit: int = Query(default=10, ge=1, le=50), offset: int = Query(default=0, ge=0),): result = POSTS
if category is not None: result = [post for post in result if post.category == category]
return result[offset : offset + limit]对应的 api/data/posts.json 可以这样写:
[ { "slug": "fastapi-basics", "title": "FastAPI 入门:给寒柳别苑搭一个小型 API", "category": "尺蠖", "tags": ["Python", "FastAPI"], "content": "这是一篇 FastAPI 学习记录。" }]这样,写完一篇新文章后,只要把摘要追加到 JSON,重新部署 API,文章列表就会同步更新。后续如果接入数据库,还可以把这个文件替换成数据库查询,而不必改变调用方的接口格式。
这里出现了三种不同来源的参数:
| 参数 | 例子 | FastAPI 的判断方式 |
|---|---|---|
| 路径参数 | /api/posts/{slug} | 参数名出现在路径模板中 |
| 查询参数 | ?category=尺蠖&limit=10 | 普通函数参数,且不属于路径 |
| 请求体 | {"message": "..."} | 参数类型是 Pydantic 模型 |
查询接口可以这样调用:
curl.exe "https://api.zylatent.com/api/posts?category=尺蠖&limit=10"Query(ge=1, le=50) 表示 limit 的最小值是 1,最大值是 50。如果传入 limit=0,FastAPI 会自动返回 422,而不是让错误参数继续进入业务逻辑。
7. 路径参数和 404:文章详情接口
列表接口只能返回摘要,详情接口需要根据 slug 找到一篇文章:
from fastapi import HTTPException
class PostDetail(PostSummary): content: str
@app.get("/api/posts/{slug}", response_model=PostDetail)def get_post(slug: str): post = next((post for post in POSTS if post.slug == slug), None)
if post is None: raise HTTPException(status_code=404, detail="文章不存在")
return post访问:
curl.exe https://api.zylatent.com/api/posts/fastapi-basics不存在的 slug:
curl.exe https://api.zylatent.com/api/posts/not-found服务会返回:
{ "detail": "文章不存在"}这比在函数里直接返回一个空字典更好,因为调用方能通过 404 明确知道资源不存在。
8. POST 请求:给网站加入留言接口
GET 主要用于读取资源,POST 常用于提交新数据。现在设计一个简化的留言接口:
from datetime import datetime, timezonefrom pydantic import Field
class FeedbackCreate(BaseModel): name: str = Field(min_length=1, max_length=30) message: str = Field(min_length=1, max_length=1000) page: str = Field(default="/", pattern=r"^/")
class FeedbackOut(FeedbackCreate): id: int created_at: datetime
FEEDBACKS: list[FeedbackOut] = []
@app.post("/api/feedback", response_model=FeedbackOut, status_code=201)def create_feedback(payload: FeedbackCreate): feedback = FeedbackOut( id=len(FEEDBACKS) + 1, created_at=datetime.now(timezone.utc), **payload.model_dump(), ) FEEDBACKS.append(feedback) return feedback提交请求:
curl.exe -X POST https://api.zylatent.com/api/feedback ` -H "Content-Type: application/json" ` -d '{"name":"访客","message":"这篇文章很有帮助。","page":"/blog/fastapi-basics/"}'PowerShell 中反引号用于换行。如果不想处理换行,也可以直接写成一行。
这个模型同时承担三项工作:
- 把 JSON 请求体解析成 Python 对象;
- 校验字符串长度和
page的格式; - 把请求结构显示在
/docs中。
这个示例把留言放在内存列表里,服务重启后数据会消失,也没有验证码、频率限制和持久化存储。它适合学习请求模型,不适合直接作为公开留言系统上线。
9. 422、404 和 500:错误应该怎么读
422:请求格式不符合模型
例如漏掉 message:
curl.exe -X POST https://api.zylatent.com/api/feedback ` -H "Content-Type: application/json" ` -d '{"name":"访客"}'这是请求方的问题。FastAPI 会在业务函数执行前完成校验,并返回 422 以及具体字段的位置。你可以直接对线上演示接口运行上面的命令,不会创建留言。
404:资源不存在
这是我们主动通过 HTTPException 返回的明确结果,常见于文章、用户或文件找不到的情况。
500:服务端代码真的出错了
如果把 payload.message 手误写成 payload.msg,程序会触发属性错误。开发环境中可以查看 traceback;生产环境中不应该把完整堆栈返回给访客。
读 traceback 时,我会先看最后一行,再回到自己代码的文件名和行号:
- 最后一行通常告诉你错误类型和直接原因;
- 往上找到项目目录中的代码行;
- 再判断是请求数据不对,还是服务内部状态不对。
报错不是事故本身,而是定位事故的线索。
10. 让 zylatent.com 调用 API:CORS 与部署边界
如果前端和 API 使用同一个 origin,例如:
https://zylatent.com 前端https://zylatent.com/api FastAPI 反向代理浏览器通常不需要额外的跨域配置。
如果以后把 API 单独部署在:
https://api.zylatent.com那么前端从 zylatent.com 请求 api.zylatent.com 就属于跨域,需要显式配置允许的 origin:
from fastapi.middleware.cors import CORSMiddleware
origins = [ "https://zylatent.com", "https://www.zylatent.com",]
app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["GET", "POST"], allow_headers=["Content-Type"],)前端调用可以是:
const response = await fetch("https://api.zylatent.com/api/site");const site = await response.json();console.log(site.name);不要在学习阶段直接写 allow_origins=["*"] 然后忘记收紧。公开 API 的 origin、认证、速率限制和日志策略,都应该随着部署环境明确配置。
11. 从示例代码到真实项目,还缺什么
为了在一篇入门文章里看清完整链路,接口逻辑暂时集中在 main.py,文章数据已经单独放到 JSON。真实项目可以进一步拆分:
fastapi-zylatent/├── app/│ ├── main.py # 创建 FastAPI 应用│ ├── models.py # Pydantic 请求和响应模型│ ├── routers/│ │ ├── site.py # 站点接口│ │ ├── posts.py # 文章接口│ │ └── feedback.py # 留言接口│ ├── services/ # 业务逻辑│ └── db.py # 数据库连接├── tests/├── pyproject.toml└── uv.lock下一步通常是:
- 用 SQLModel 或 SQLAlchemy 接入数据库;
- 用依赖注入共享数据库会话;
- 为接口添加测试;
- 加入认证和权限;
- 配置生产服务器与反向代理;
- 用环境变量管理域名、密钥和数据库连接串。
这些内容不应该在第一篇入门文章里全部展开。先把“请求如何进入路由,数据如何被校验,响应如何返回”这条链路走通,后续的数据库和认证才有落脚点。
12. 学习记录
这次学习让我对 FastAPI 的理解从“一个写 API 的 Python 框架”变得更具体:
- 路由装饰器把 HTTP 方法和路径绑定到 Python 函数;
- 类型标注让路径参数、查询参数和请求体的边界变得清楚;
- Pydantic 模型把数据合约、校验和接口文档连接起来;
HTTPException让错误成为 API 合约的一部分;/docs让接口可以在没有额外前端页面的情况下被观察和测试;- 域名只是访问入口,真正的部署还需要服务器、反向代理、跨域和安全配置。
对我来说,比较有价值的不是记住几条命令,而是把一个熟悉的网站想象成一组可以被其他程序调用的资源:站点资料、文章列表和留言,都可以先用清晰的接口表达出来。
13. 让读者实际调用一次
现在 API 已经部署,浏览器端可以直接调用它。下面这个小面板只开放读取接口,点击按钮后会把真实响应显示出来:
动手试试:调用寒柳别苑 API
当前地址:https://api.zylatent.com
它本质上就是下面这段 JavaScript:
const response = await fetch("https://api.zylatent.com/api/site");const site = await response.json();
console.log(site);如果你更熟悉 Python,也可以直接运行下面的脚本。先安装一个轻量的 HTTP 客户端:
python -m pip install requests然后保存为 try_zylatent_api.py:
import requests
BASE_URL = "https://api.zylatent.com"
def get_json(path: str, **kwargs): response = requests.get(f"{BASE_URL}{path}", timeout=10, **kwargs) response.raise_for_status() return response.json()
site = get_json("/api/site")print(f"{site['name']}:{site['description']}")print("主题:", "、".join(site["topics"]))
posts = get_json( "/api/posts", params={"category": "尺蠖", "limit": 10},)
for index, post in enumerate(posts, start=1): print(f"{index}. {post['title']} [{post['slug']}]")这个例子对应的就是一次普通的 GET 请求:Python 负责发出请求和解析 JSON,FastAPI 负责读取文章文件、筛选参数并返回结构化响应。你也可以把 /api/posts 换成 /api/posts/fastapi-basics,进一步试试路径参数。
也可以在终端里调用同一个接口:
curl.exe https://api.zylatent.com/healthcurl.exe "https://api.zylatent.com/api/posts?category=尺蠖&limit=10"为了让这个公开演示保持简单,页面暂时只调用 GET 接口;读者可以通过线上 /docs 或命令行体验留言接口。当前留言只保存在服务器进程的内存中,重启后会消失,也没有验证码、频率限制和内容审核,因此它仍然只是学习演示,不应直接当作正式留言系统使用。
