response-handling-status-codes

📂 Phase: Phase 1 - Rapid Foundation Building (Basics) 🔗 Related Chapters: path-query-parameters · request-body-handling

When building a Web API, the request is only half the story, the response is what is actually delivered to the client. FastAPI provides very flexible response control capabilities, from the simplest data return, to status codes, response headers, exception-handling, and even completely customized response types, you can easily master it. This article connects all the key knowledge points together, allowing you to learn "response processing" in one step.


1. Response Model (Response Model)

Basic implementation and field filtering

Many times, we receive more request data than we ultimately return. For example, the user registration interface receivespassword, but the password must not be passed back to the front end in the response. FastAPI useresponse_modelParameters to declare the "return contract" and automatically filter out fields that should not appear.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

# 最终返回给前端的用户信息
class PublicUser(BaseModel):
    id: int
    name: str
    email: str

# 注册时接收的用户数据(含密码)
class UserCreate(BaseModel):
    name: str
    email: str
    password: str

@app.post("/users/", response_model=PublicUser)
async def register(user: UserCreate):
    # 模拟创建用户,密码会被自动过滤
    return {"id": 1, **user.model_dump()}

In the above code, althoughreturncontainspassword, but becauseresponse_model=PublicUser, FastAPI will usePublicUserThe model "cleans" the output data, leaving onlyidnameandemail. To the front end, the password is never exposed.

Core value of the model

Don’t think that the response model is as simple as a “filter field”. It can also bring three major benefits:

  1. Data conversion and verification: returned by the databaseDecimalFields can be automatically converted to JSON friendlyfloat; Missing optional fields will be filled with default values; if the field type is incorrect, an error will be reported directly to prevent dirty data from flowing out.
  2. Automatically generate Swagger / OpenAPI documents: Front-end developers can understand field types, required fields, and example values ​​by looking at the document, and communication costs plummet.
  3. Development Contract: The front-end and back-end share the Pydantic model. Once the interface fields change, both parties must update the model synchronously, which is more secure.

Flexible field control

In addition to using an independent response model, FastAPI also provides three parameters that allow you to flexibly adjust the output fields in a single interface:

  • response_model_exclude:Exclude certain fields
  • response_model_include: Contains only specified fields (highest priority)
  • response_model_exclude_unset: Exclude fields that are not explicitly assigned a value
from fastapi import status

# 1. 返回用户但隐藏邮箱
@app.post("/users/", response_model=PublicUser, response_model_exclude={"email"})
async def register_hide_email(user: UserCreate):
    return {"id": 1, **user.model_dump()}

# 2. 只返回已设置的字段(未赋值的字段会被忽略)
@app.get("/users/{user_id}", response_model=PublicUser, response_model_exclude_unset=True)
async def get_user(user_id: int):
    # 假设缓存中只有 id 和 name
    return {"id": user_id, "name": "张三"}

# 3. 只返回商品的名字和价格
@app.get("/items/{item_id}", response_model=Item, response_model_include={"name", "price"})
async def get_item_summary(item_id: int):
    return get_full_item_from_db(item_id)

whenresponse_model_exclude_unset=True, even if the model definesemailfield, but as long as it is not explicitly assigned a value in the returned dictionary, it will not appear in the response. In this way, data packetization after "partial update" can be flexibly realized.


2. Status Code (Status Code)

Unified style setting method

Using status codes to tell the client what happened is basic RESTful etiquette. FastAPI has two ways of writing:

  1. Write numbers directly (small but difficult to maintain)
  2. Usefastapi.statusConstant (with IDE automatic completion, clear semantics, highly recommended)
from fastapi import status

@app.post("/items/", status_code=201)
async def create_item_raw(item: ItemCreate):
    return {"id": 1, **item.model_dump()}

# 推荐方式:用常量
@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_item_pro(item_id: int):
    # 204 规范要求响应体为空,直接 return 或 return None
    return

return204It means that the deletion is successful, but the response body has no content and the browser will not jump to the page.

High Frequency Status Code Cheat Sheet

Remember the following status codes, which are basically enough for daily development:

Status codeConstantDescription (RESTful semantics)
200HTTP_200_OKQuery, update, partial update successful
201HTTP_201_CREATEDResource creation successful (such as POST registration)
204HTTP_204_NO_CONTENTDelete successfully or empty query
400HTTP_400_BAD_REQUESTRequest parameter format/logic error
401HTTP_401_UNAUTHORIZEDNot logged in or Token is invalid
403HTTP_403_FORBIDDENLogged in but do not have permission to operate this resource
404HTTP_404_NOT_FOUNDResource does not exist
422HTTP_422_UNPROCESSABLE_ENTITYFastAPI / Pydantic verification failed
500HTTP_500_INTERNAL_SERVER_ERRORServer internal uncontrollable error

💡 Tip: If you encounter a client parameter transfer error, you can return400; If the data verification fails, it will be handed over to FastAPI and automatically returned.422; Used for permission issues403, to prevent attackers from guessing whether resources exist404cover.


3. Directly operate the Response object

when simpleresponse_modelandstatus_codeWhen the needs cannot be met (such as dynamically modifying response headers and setting cookies), you can use it directlyResponseor its subclasses.

Fully custom JSON response

JSONResponseIt is a powerful tool for customizing JSON responses. It allows you to temporarily change the status code and add business error headers:

from fastapi.responses import JSONResponse

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id > 100:
        return JSONResponse(
            status_code=status.HTTP_404_NOT_FOUND,
            content={"code": "ITEM_NOT_FOUND", "message": "商品不存在"},
            headers={"X-Error-Code": "ITEM_NOT_FOUND"}
        )
    return {"item_id": item_id, "name": "键盘"}

Although I returned hereJSONResponse, but FastAPI will no longer be used at allresponse_modelGo to secondary processing, so you can freely control all the details of the response.

Configure response headers and cookies

In addition to returning directlyJSONResponse, you can also putResponseThe object is injected as a parameter, so that while returning the Pydantic model normally, additional header information is added to the response:

from fastapi import Response

@app.get("/items/")
async def list_items(response: Response):
    response.headers["X-Total-Count"] = "500"
    return [{"id": 1, "name": "键盘"}]

@app.post("/login/")
async def login(response: Response):
    # httponly=True 防止前端 JS 读取,减少 XSS 风险
    response.set_cookie(
        key="session_id",
        value="secure_token_123",
        httponly=True,
        max_age=1800,      # 30 分钟过期
        samesite="lax"     # 防止 CSRF 攻击
    )
    return {"message": "登录成功"}

Something to note: once you returnResponseObject, FastAPI will not perform any serialization or model verification on the data. If you want to return the model normally and add a response header, remember to use "InjectionResponseparameter + directreturndictionary or model" method.


4. Switch different response types

By default FastAPI returns JSON, but you can definitely passresponse_classParameters return HTML, plain text, files, and even streaming data.

from fastapi.responses import (
    HTMLResponse,
    PlainTextResponse,
    FileResponse,
    StreamingResponse,
    RedirectResponse
)
import io

# 1. HTML 页面
@app.get("/html/", response_class=HTMLResponse)
async def get_home():
    return """
    <html>
        <head><title>我的小站</title></head>
        <body><h1>Hello FastAPI 🚀</h1></body>
    </html>
    """

# 2. 纯文本(健康检查)
@app.get("/text/", response_class=PlainTextResponse)
async def get_health():
    return "OK"

# 3. 文件下载(自动设置 Content-Disposition 头)
@app.get("/download/report")
async def download_report():
    return FileResponse(
        path="./static/report.pdf",
        filename="2024年度报告.pdf",
        media_type="application/pdf"
    )

# 4. 流式返回(适合大文件或实时日志)
@app.get("/stream/logs")
async def stream_logs():
    async def generate_logs():
        for i in range(10):
            yield f"[INFO] 第 {i} 条日志\n"
    return StreamingResponse(generate_logs(), media_type="text/plain")

# 5. 重定向
@app.get("/old-docs")
async def redirect_to_new_docs():
    return RedirectResponse(url="/docs")

⚠️ Note: If the return type is notdictor Pydantic model, and you forgot to specifyresponse_class, FastAPI will try to serialize it to JSON, which may cause errors. Use rightresponse_classThis type of problems can be avoided.


5. Standard exception-handling

Built-in HTTP exception throwing

The most commonly used exception-handling isHTTPException, which terminates the current request anywhere and returns a clear error:

from fastapi import HTTPException

@app.get("/items/{item_id}")
async def read_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(
            status_code=status.HTTP_404_NOT_FOUND,
            detail="商品不存在",
            headers={"X-Error-Code": "ITEM_NOT_FOUND"}
        )
    return items_db[item_id]

detailIt can be a string or a dictionary/list to facilitate the definition of business error codes and additional information.

Business-level custom exceptions

Business logic errors such as "Insufficient Inventory", useHTTPExceptionAlthough it is also possible, define a dedicated exception class and then pass@app.exception_handler()Registering a handler function will make the code more expressive:

from fastapi import Request

class OutOfStockException(Exception):
    def __init__(self, item_id: int, stock: int):
        self.item_id = item_id
        self.stock = stock

@app.exception_handler(OutOfStockException)
async def out_of_stock_handler(request: Request, exc: OutOfStockException):
    return JSONResponse(
        status_code=status.HTTP_400_BAD_REQUEST,
        content={
            "code": "OUT_OF_STOCK",
            "message": f"商品 {exc.item_id} 库存不足,当前剩余 {exc.stock} 件"
        }
    )

@app.post("/items/{item_id}/buy")
async def buy_item(item_id: int):
    if items_db[item_id]["stock"] < 1:
        raise OutOfStockException(item_id, 0)
    items_db[item_id]["stock"] -= 1
    return {"message": "购买成功"}

Global exception interception

The production environment is most afraid of exposing internal exceptions (database connection failure, third-party API timeout) directly to the front end. We can intercept Pydantic verification exceptions and unknown ones respectivelyException, unified format output:

from fastapi.exceptions import RequestValidationError

# 定制校验失败的响应格式
@app.exception_handler(RequestValidationError)
async def validation_handler(request: Request, exc: RequestValidationError):
    return JSONResponse(
        status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
        content={"code": "VALIDATION_ERROR", "details": exc.errors()}
    )

# 兜底所有未捕获的异常
@app.exception_handler(Exception)
async def global_handler(request: Request, exc: Exception):
    return JSONResponse(
        status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
        content={"code": "INTERNAL_ERROR", "message": "服务器开小差了,请稍后重试"}
    )

🔒 Security Tip: Do not return in global interceptionexcspecific content (such as stack information) to avoid leaking sensitive details.


6. Multiple status codes and custom response models

An interface may return multiple status codes, and the response body structure corresponding to each status code is also different (for example, 200 returnsdata,404 returnerrorinformation). It can be used at this timeresponsesParameters to describe exactly these branches in the Swagger document.

from pydantic import BaseModel

class SuccessResponse(BaseModel):
    code: str = "SUCCESS"
    data: dict | None = None

class ErrorResponse(BaseModel):
    code: str
    message: str

@app.get(
    "/items/{item_id}",
    responses={
        200: {"model": SuccessResponse, "description": "查询成功"},
        404: {"model": ErrorResponse, "description": "商品不存在"}
    }
)
async def read_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(
            status_code=404,
            detail={"code": "ITEM_NOT_FOUND", "message": "商品不存在"}
        )
    return {"code": "SUCCESS", "data": items_db[item_id]}

In this way, in the automatically generated document, the front end can see the response examples of each status code, and feel more confident when calling the API.


7. Universal paging response solution

List interfaces almost always require paging. FastAPI + Pydantic's generic support allows us to write a "define once, use everywhere" universal paging model.

from typing import Generic, List, TypeVar

T = TypeVar("T")

class PaginatedResponse(BaseModel, Generic[T]):
    items: List[T]        # 当前页的数据列表
    total: int            # 总记录数
    page: int             # 当前页码
    page_size: int        # 每页条数
    total_pages: int      # 总页数

@app.get("/items/", response_model=PaginatedResponse[Item])
async def list_items(page: int = 1, page_size: int = 10):
    # 防止大数据量拖垮服务
    page_size = min(page_size, 100)
    all_items = list(items_db.values())
    start = (page - 1) * page_size
    end = start + page_size
    total_pages = (len(all_items) + page_size - 1) // page_size
    return {
        "items": all_items[start:end],
        "total": len(all_items),
        "page": page,
        "page_size": page_size,
        "total_pages": total_pages
    }

GenericsPaginatedResponse[Item]Tell FastAPI that the data for each page isItemA list of types, corresponding examples will be generated correctly in the document, and type checking will be more stringent.

🧠 Best Practice: Always Givepage_sizeSet an upper limit (such as 100) to prevent malicious or unexpected large requests from causing a sudden increase in database pressure.


8. Summary

We start from the most basic response model and go all the way to exception-handling and paging, basically covering all the response knowledge required to build a professional API. Finally, here’s a cheat sheet to help you quickly review:

Functional scenariosCore usage/classes/parameters
Declare response contract + filter fieldresponse_model=PublicUser
Flexible adjustment of individual interface fieldsresponse_model_exclude/include/unset
Status codes with unified semanticsfastapi.status.HTTP_201_CREATED
Fully customized JSON responsefastapi.responses.JSONResponse
Add response header/set cookieInjectResponseParameters
Switch response typeresponse_class=HTMLResponseetc
Throwing standard business exceptionsraise HTTPException
Custom business exceptionDefine class +@app.exception_handler()
Global interceptionInterceptionException
Multiple status codes + multiple model documentsresponses={200: {"model": Success}}
Universal PaginationPydantic GenericsPaginatedResponse[T]

After mastering these skills, your FastAPI application can output a standardized, safe, and easy-to-maintain API. In the next step, you can continue to learn dependency injection and middleware to take the architecture further 🚀.