Fastapi——Dependency、SwagUI、自定义状态码




2020-04-21

blog_main_img

`FastAPI` 让人上头的地方,不只是它快,而是很多平时写接口时容易散掉的东西,它都帮你收得很利落。比如: - 公共逻辑不想每个路由都手写一遍 - 返回状态码不想总是糊成一个 `200`

为什么这三个点值得放一起讲

很多项目刚开头时,接口都很轻,怎么写都像能跑。可一旦路由多起来,问题就开始冒出来:

  • token 校验每个接口写一遍
  • 数据库会话创建和释放到处复制
  • 文档页默认能用,但团队调试时总觉得差点意思
  • 明明是“创建成功”,结果前端永远只看到 200

FastAPI 这套能力正好能把这些地方收一下。

说得更直接一点:

  • Dependency 解决“重复逻辑到处飘”
  • Swagger UI 解决“文档页只是存在,但不好用”
  • 状态码设计解决“接口返回值会说话,但还不够准”

Dependency:别再把公共逻辑塞进每个路由里

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,
    }

这个例子看着不复杂,但已经把一个重要思路定下来了:

  • 公共查询参数可以抽成 dependency
  • 路由函数不用再自己解析一遍
  • 后面要改默认分页,只改一处就行

这比每个接口自己收 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 很值钱的地方。它不只是少写几行,而是把资源生命周期也一起整理了。

权限校验也很适合 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,或者继续补角色判断,也不用把路由函数翻一遍。

dependency 还能一层套一层

这点很容易被低估。FastAPI 的 dependency 不是平铺的,它可以继续依赖别的 dependency。

比如:

  • get_current_user() 依赖 parse_token()
  • get_admin_user() 再依赖 get_current_user()

这样权限链就能写得很自然,而不是所有东西挤进一个巨型函数里。

Swagger UI:别让文档页只是“默认开着”

很多人第一次用 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
  • OpenAPI 描述文件也有了固定出口

对团队协作来说,这种小收口很重要。接口不只是“能调”,还得“容易说明白”。

调 Swagger UI 的展示参数

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 示意图

想更定制一点,可以自己接文档页

有时候默认的 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",
    )

这种写法在下面这些场景很有用:

  • 想把默认文档页入口隐藏掉
  • 想统一公司内部 API 门户风格
  • 想给文档页加额外说明或导航

它不是必须,但一旦你的项目不再是“只有自己调”,就会很顺手。

自定义状态码:别把所有成功都塞进 200

如果一个接口创建了新资源,返回 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

很多项目一开始没想太多,接口一出错就让异常自然冒泡,最后统一变成 500。这当然能工作,但信息含量太低。

FastAPIHTTPException 很适合把这件事说清楚。

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}

这类写法的好处在于:

  • 客户端知道自己是参数错了,还是资源不存在
  • Swagger UI 里能直接看到异常语义
  • 日后做错误码映射时也更自然

再往前走一步:统一异常格式

如果你不想每个异常都长得不一样,可以自己加异常处理器。

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),
        },
    )

这样你后面的接口就能保持统一语气:

  • 状态码表达 HTTP 层语义
  • error_code 表达业务层语义
  • message 给人看

这会比一堆随手拼出来的报错 JSON 干净很多。

状态码流转图

把三件事揉进一个小接口

如果把 DependencySwagger 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,
        },
    )

这段小代码里其实已经把几件关键事串起来了:

  • 认证逻辑抽成 dependency
  • 分页参数抽成 dependency
  • 文档页地址和展示参数收口
  • 创建接口明确返回 201

这种结构很适合做项目的第一版骨架,因为它不是“先跑起来再说”,而是一开始就尽量别散。

用 FastAPI 写接口时,一个很实用的习惯

如果你想让接口项目从一开始就更稳,我会建议你把下面三个动作当成默认习惯:

  • 只要某段逻辑会在多个路由出现,就优先考虑 dependency
  • 只要接口会给团队或前端调,就别放任 Swagger UI 维持默认样子
  • 只要接口有明确业务动作,就给它匹配合适的状态码

这三个习惯单独看都不大,但放在一起,会让项目明显更整洁。

如果你已经在写 FastAPI,这三块值得尽早养成习惯。等路由一多,它们给你的回报会非常直接。