2020-04-21
`FastAPI` 让人上头的地方,不只是它快,而是很多平时写接口时容易散掉的东西,它都帮你收得很利落。比如: - 公共逻辑不想每个路由都手写一遍 - 返回状态码不想总是糊成一个 `200`
很多项目刚开头时,接口都很轻,怎么写都像能跑。可一旦路由多起来,问题就开始冒出来:
200FastAPI 这套能力正好能把这些地方收一下。
说得更直接一点:
Dependency 解决“重复逻辑到处飘”Swagger UI 解决“文档页只是存在,但不好用”FastAPI 里最常用也最顺手的一个特性,就是 Depends()。
它的味道不是“魔法”,而更像“把公共逻辑注册成可组合组件”。路由只关心自己要什么,不关心这份东西是怎么准备出来的。
先看一个轻量例子:
from fastapi import Depends, FastAPI
app = FastAPI()
def common_query(q: str | None = None, page: int = 1, size: int = 20):
return {"q": q, "page": page, "size": size}
@app.get("/articles")
def list_articles(params: dict = Depends(common_query)):
return {
"message": "query accepted",
"params": params,
}
这个例子看着不复杂,但已经把一个重要思路定下来了:
这比每个接口自己收 q/page/size 要干净得多。
在接口项目里,数据库会话是很典型的 dependency 场景。
from collections.abc import Generator
from fastapi import Depends, FastAPI
from sqlalchemy.orm import Session
app = FastAPI()
def get_db() -> Generator[Session, None, None]:
db = SessionLocal()
try:
yield db
finally:
db.close()
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
user = db.get(User, user_id)
return {"user_id": user_id, "user": user}
这里最舒服的一点是:yield 把“拿资源”和“释放资源”一起包好了。
路由函数只拿到 db,不用再关心:
这就是 dependency 很值钱的地方。它不只是少写几行,而是把资源生命周期也一起整理了。
再往前走一步,权限校验也特别适合塞进 Depends()。
from fastapi import Depends, FastAPI, Header, HTTPException, status
app = FastAPI()
def get_current_user(x_token: str = Header(...)):
if x_token != "debug-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="invalid token",
)
return {"username": "codex-admin", "role": "editor"}
@app.get("/profile")
def read_profile(current_user: dict = Depends(get_current_user)):
return {"current_user": current_user}
这个结构的好处很明显:
后面如果你要把 token 校验换成 JWT,或者继续补角色判断,也不用把路由函数翻一遍。
这点很容易被低估。FastAPI 的 dependency 不是平铺的,它可以继续依赖别的 dependency。
比如:
get_current_user() 依赖 parse_token()get_admin_user() 再依赖 get_current_user()这样权限链就能写得很自然,而不是所有东西挤进一个巨型函数里。
很多人第一次用 FastAPI,都会被 /docs 惊一下:这文档页也太省心了。
确实,开箱即用是它的一大优点。但如果只是停在默认页面,其实还有不少可玩的空间。
先把文档地址和标题收一下:
from fastapi import FastAPI
app = FastAPI(
title="Content Center API",
description="后台内容服务接口",
version="0.1.0",
docs_url="/swagger",
redoc_url="/redoc",
openapi_url="/openapi.json",
)
这段配置做了几件很实用的小事:
/docs对团队协作来说,这种小收口很重要。接口不只是“能调”,还得“容易说明白”。
FastAPI 还支持直接给 Swagger UI 丢参数。
from fastapi import FastAPI
app = FastAPI(
swagger_ui_parameters={
"defaultModelsExpandDepth": -1,
"docExpansion": "list",
"displayRequestDuration": True,
"filter": True,
"syntaxHighlight.theme": "monokai",
}
)
这些配置很适合把文档页收得更利落一点:
defaultModelsExpandDepth=-1 可以把右侧 schema 面板先收起来docExpansion="list" 让接口折叠得更整齐displayRequestDuration=True 调试时能顺手看看耗时filter=True 路由一多以后特别好用如果你的接口列表很长,这几个参数能让 Swagger UI 的可读性一下子上来。
有时候默认的 Swagger UI 还不够,你想替换页面标题、引入自定义 JS 或 CSS,这时候可以直接用 get_swagger_ui_html()。
from fastapi import FastAPI
from fastapi.openapi.docs import get_swagger_ui_html
from fastapi.responses import HTMLResponse
app = FastAPI(docs_url=None)
@app.get("/swagger", include_in_schema=False)
def custom_swagger() -> HTMLResponse:
return get_swagger_ui_html(
openapi_url=app.openapi_url,
title="Content Center Swagger UI",
swagger_js_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui-bundle.js",
swagger_css_url="https://cdn.jsdelivr.net/npm/swagger-ui-dist/swagger-ui.css",
)
这种写法在下面这些场景很有用:
它不是必须,但一旦你的项目不再是“只有自己调”,就会很顺手。
如果一个接口创建了新资源,返回 201 会比 200 更清楚。
如果一个接口删除成功却没内容,204 往往比“返回一个空 JSON”更干净。
状态码这件事,说到底是在帮接口表达语义。它不是装饰,而是协议的一部分。
最简单的方式,就是在装饰器里写:
from fastapi import FastAPI, status
app = FastAPI()
@app.post("/posts", status_code=status.HTTP_201_CREATED)
def create_post():
return {"message": "post created"}
这种写法的优点很明显:
JSONResponse 精准控制返回如果你想按业务分支返回不同状态码,可以用 JSONResponse。
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI()
@app.put("/posts/{post_id}")
def upsert_post(post_id: int, payload: dict):
existed = post_id % 2 == 0
if existed:
return JSONResponse(
status_code=200,
content={"message": "post updated", "post_id": post_id},
)
return JSONResponse(
status_code=201,
content={"message": "post created", "post_id": post_id},
)
这个模式在“更新或创建”“命中缓存或重新生成”“同步成功但结果不同”这些场景里很好用。
很多项目一开始没想太多,接口一出错就让异常自然冒泡,最后统一变成 500。这当然能工作,但信息含量太低。
FastAPI 的 HTTPException 很适合把这件事说清楚。
from fastapi import FastAPI, HTTPException, status
app = FastAPI()
@app.get("/orders/{order_id}")
def get_order(order_id: int):
if order_id <= 0:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="order_id must be positive",
)
if order_id == 404:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="order not found",
)
return {"order_id": order_id}
这类写法的好处在于:
如果你不想每个异常都长得不一样,可以自己加异常处理器。
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
app = FastAPI()
class BizError(Exception):
def __init__(self, code: str, message: str, status_code: int = 400):
self.code = code
self.message = message
self.status_code = status_code
@app.exception_handler(BizError)
async def biz_error_handler(request: Request, exc: BizError):
return JSONResponse(
status_code=exc.status_code,
content={
"ok": False,
"error_code": exc.code,
"message": exc.message,
"path": str(request.url.path),
},
)
这样你后面的接口就能保持统一语气:
error_code 表达业务层语义message 给人看这会比一堆随手拼出来的报错 JSON 干净很多。
如果把 Dependency、Swagger UI 和状态码放进同一个迷你项目,它大概会长这样:
from fastapi import Depends, FastAPI, Header, HTTPException, status
from fastapi.responses import JSONResponse
app = FastAPI(
title="Article Service",
docs_url="/swagger",
swagger_ui_parameters={
"docExpansion": "list",
"filter": True,
},
)
def get_token(x_token: str = Header(...)):
if x_token != "debug-token":
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="invalid token",
)
return x_token
def get_pagination(page: int = 1, size: int = 10):
return {"page": page, "size": size}
@app.get("/articles")
def list_articles(
token: str = Depends(get_token),
pagination: dict = Depends(get_pagination),
):
return {"token": token, "pagination": pagination, "items": []}
@app.post("/articles")
def create_article(payload: dict, token: str = Depends(get_token)):
return JSONResponse(
status_code=status.HTTP_201_CREATED,
content={
"message": "article created",
"token": token,
"payload": payload,
},
)
这段小代码里其实已经把几件关键事串起来了:
201这种结构很适合做项目的第一版骨架,因为它不是“先跑起来再说”,而是一开始就尽量别散。
如果你想让接口项目从一开始就更稳,我会建议你把下面三个动作当成默认习惯:
这三个习惯单独看都不大,但放在一起,会让项目明显更整洁。
如果你已经在写 FastAPI,这三块值得尽早养成习惯。等路由一多,它们给你的回报会非常直接。