Python 9 min read

FastAPI Tutorial Part 4: Response Models and Status Codes - Control Your API Output

Learn to control FastAPI responses with response models, status codes, and headers. Master data filtering, multiple response types, and proper HTTP semantics.

MR

Moshiour Rahman

Advertisement

Why Response Models Matter

Response models control what data your API returns. They ensure consistent output, filter sensitive data, and generate accurate API documentation.

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class UserIn(BaseModel):
    username: str
    email: str
    password: str  # Sensitive!

class UserOut(BaseModel):
    username: str
    email: str
    # password excluded from response

@app.post("/users", response_model=UserOut)
def create_user(user: UserIn):
    # Password is filtered out automatically
    return user

Basic Response Models

Defining Response Models

from pydantic import BaseModel
from typing import List, Optional
from datetime import datetime

class ItemBase(BaseModel):
    name: str
    price: float
    description: Optional[str] = None

class ItemCreate(ItemBase):
    """Used for creating items (request)."""
    pass

class ItemResponse(ItemBase):
    """Used for returning items (response)."""
    id: int
    created_at: datetime
    is_available: bool = True

@app.post("/items", response_model=ItemResponse)
def create_item(item: ItemCreate):
    return {
        "id": 1,
        "name": item.name,
        "price": item.price,
        "description": item.description,
        "created_at": datetime.now(),
        "is_available": True
    }

List Responses

@app.get("/items", response_model=List[ItemResponse])
def list_items():
    return [
        {"id": 1, "name": "Item 1", "price": 10.0, "created_at": datetime.now()},
        {"id": 2, "name": "Item 2", "price": 20.0, "created_at": datetime.now()}
    ]

Optional Response Model

@app.get("/items/{item_id}", response_model=Optional[ItemResponse])
def get_item(item_id: int):
    item = get_item_from_db(item_id)
    if not item:
        return None
    return item

Filtering Response Data

Exclude Unset Fields

class ItemUpdate(BaseModel):
    name: Optional[str] = None
    price: Optional[float] = None
    description: Optional[str] = None

@app.patch("/items/{item_id}", response_model=ItemResponse)
def update_item(item_id: int, item: ItemUpdate):
    # Only return fields that were actually set
    stored = get_item_from_db(item_id)
    update_data = item.model_dump(exclude_unset=True)
    updated = stored.copy(update=update_data)
    return updated

Response Model Parameters

class User(BaseModel):
    id: int
    username: str
    email: str
    hashed_password: str
    is_active: bool = True
    role: str = "user"

# Exclude specific fields
@app.get("/users/{user_id}", response_model=User, response_model_exclude={"hashed_password"})
def get_user(user_id: int):
    return users_db[user_id]

# Include only specific fields
@app.get("/users/{user_id}/public", response_model=User, response_model_include={"id", "username"})
def get_user_public(user_id: int):
    return users_db[user_id]

# Exclude defaults (unset optional fields)
@app.get("/users", response_model=List[User], response_model_exclude_unset=True)
def list_users():
    return list(users_db.values())

# Exclude None values
@app.get("/items", response_model=List[ItemResponse], response_model_exclude_none=True)
def list_items():
    return items

HTTP Status Codes

Common Status Codes

CodeNameUse Case
200OKSuccessful GET, PUT, PATCH
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestInvalid request data
401UnauthorizedAuthentication required
403ForbiddenPermission denied
404Not FoundResource doesn’t exist
422Unprocessable EntityValidation error
500Internal Server ErrorServer error

Setting Status Codes

from fastapi import FastAPI, status

app = FastAPI()

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

# Using status constants (recommended)
@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: UserCreate):
    return {"id": 1, **user.model_dump()}

@app.delete("/items/{item_id}", status_code=status.HTTP_204_NO_CONTENT)
def delete_item(item_id: int):
    del items_db[item_id]
    return None  # 204 should have no body

Dynamic Status Codes

from fastapi import Response

@app.post("/items")
def create_item(item: ItemCreate, response: Response):
    if item_exists(item.name):
        response.status_code = status.HTTP_200_OK
        return get_existing_item(item.name)
    else:
        response.status_code = status.HTTP_201_CREATED
        return create_new_item(item)

Multiple Response Types

Different Response Models

from fastapi import HTTPException
from typing import Union

class ItemFull(BaseModel):
    id: int
    name: str
    price: float
    description: str
    internal_code: str

class ItemPublic(BaseModel):
    id: int
    name: str
    price: float

@app.get("/items/{item_id}", response_model=Union[ItemFull, ItemPublic])
def get_item(item_id: int, include_internal: bool = False):
    item = items_db.get(item_id)
    if not item:
        raise HTTPException(status_code=404, detail="Item not found")

    if include_internal:
        return ItemFull(**item)
    return ItemPublic(**item)

Documenting Multiple Responses

from fastapi import HTTPException

class Item(BaseModel):
    id: int
    name: str

class ErrorMessage(BaseModel):
    detail: str

@app.get(
    "/items/{item_id}",
    response_model=Item,
    responses={
        200: {"description": "Item found", "model": Item},
        404: {"description": "Item not found", "model": ErrorMessage},
        500: {"description": "Internal server error"}
    }
)
def get_item(item_id: int):
    if item_id not in items_db:
        raise HTTPException(status_code=404, detail="Item not found")
    return items_db[item_id]

Complex Response Documentation

@app.get(
    "/items",
    responses={
        200: {
            "description": "List of items",
            "content": {
                "application/json": {
                    "example": [
                        {"id": 1, "name": "Widget", "price": 9.99},
                        {"id": 2, "name": "Gadget", "price": 19.99}
                    ]
                }
            }
        },
        204: {"description": "No items found"}
    }
)
def list_items():
    return items_db.values()

Response Headers

Setting Custom Headers

from fastapi import Response

@app.get("/items")
def list_items(response: Response):
    response.headers["X-Total-Count"] = "100"
    response.headers["X-Page"] = "1"
    return items_db.values()

@app.get("/download")
def download_file(response: Response):
    response.headers["Content-Disposition"] = "attachment; filename=data.csv"
    return generate_csv()

Cache Headers

@app.get("/static-data")
def get_static_data(response: Response):
    response.headers["Cache-Control"] = "public, max-age=3600"
    response.headers["ETag"] = '"abc123"'
    return {"data": "static content"}

@app.get("/dynamic-data")
def get_dynamic_data(response: Response):
    response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
    return {"timestamp": datetime.now()}

Custom Response Classes

JSON Response

from fastapi.responses import JSONResponse

@app.get("/custom")
def custom_response():
    return JSONResponse(
        content={"message": "Custom response"},
        status_code=200,
        headers={"X-Custom": "Header"}
    )

Plain Text Response

from fastapi.responses import PlainTextResponse

@app.get("/text", response_class=PlainTextResponse)
def get_text():
    return "Hello, World!"

HTML Response

from fastapi.responses import HTMLResponse

@app.get("/page", response_class=HTMLResponse)
def get_page():
    return """
    <html>
        <head><title>My Page</title></head>
        <body><h1>Hello, FastAPI!</h1></body>
    </html>
    """

Redirect Response

from fastapi.responses import RedirectResponse

@app.get("/old-path")
def redirect_old():
    return RedirectResponse(url="/new-path")

@app.get("/external")
def redirect_external():
    return RedirectResponse(
        url="https://example.com",
        status_code=status.HTTP_307_TEMPORARY_REDIRECT
    )

Streaming Response

from fastapi.responses import StreamingResponse
import io

@app.get("/stream")
def stream_data():
    def generate():
        for i in range(100):
            yield f"data: {i}\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

@app.get("/csv")
def download_csv():
    def generate_csv():
        yield "id,name,price\n"
        for item in items_db.values():
            yield f"{item['id']},{item['name']},{item['price']}\n"

    return StreamingResponse(
        generate_csv(),
        media_type="text/csv",
        headers={"Content-Disposition": "attachment; filename=items.csv"}
    )

File Response

from fastapi.responses import FileResponse

@app.get("/download/{filename}")
def download_file(filename: str):
    file_path = f"./files/{filename}"
    return FileResponse(
        path=file_path,
        filename=filename,
        media_type="application/octet-stream"
    )

Response Model Inheritance

Base and Extended Models

class ItemBase(BaseModel):
    name: str
    price: float

class ItemCreate(ItemBase):
    pass

class ItemInDB(ItemBase):
    id: int
    created_at: datetime
    updated_at: datetime

class ItemPublic(ItemBase):
    id: int

class ItemAdmin(ItemInDB):
    internal_notes: str
    profit_margin: float

Using Different Models

@app.post("/items", response_model=ItemPublic, status_code=201)
def create_item(item: ItemCreate):
    db_item = save_to_db(item)
    return db_item  # Extra fields filtered out

@app.get("/items/{item_id}", response_model=ItemPublic)
def get_item(item_id: int):
    return items_db[item_id]

@app.get("/admin/items/{item_id}", response_model=ItemAdmin)
def admin_get_item(item_id: int):
    return items_db[item_id]

Practical Example: E-commerce API Responses

from fastapi import FastAPI, HTTPException, status, Response
from pydantic import BaseModel, Field
from typing import List, Optional
from datetime import datetime
from enum import Enum

app = FastAPI()

# Enums
class OrderStatus(str, Enum):
    pending = "pending"
    confirmed = "confirmed"
    shipped = "shipped"
    delivered = "delivered"
    cancelled = "cancelled"

# Base Models
class ProductBase(BaseModel):
    name: str
    price: float
    description: Optional[str] = None

class OrderItemBase(BaseModel):
    product_id: int
    quantity: int

# Request Models
class ProductCreate(ProductBase):
    sku: str
    cost_price: float  # Internal field

class OrderCreate(BaseModel):
    items: List[OrderItemBase]
    shipping_address: str

# Response Models
class ProductPublic(ProductBase):
    id: int
    is_available: bool

class ProductAdmin(ProductPublic):
    sku: str
    cost_price: float
    profit_margin: float
    created_at: datetime

class OrderItemResponse(OrderItemBase):
    product_name: str
    unit_price: float
    subtotal: float

class OrderResponse(BaseModel):
    id: int
    status: OrderStatus
    items: List[OrderItemResponse]
    total: float
    shipping_address: str
    created_at: datetime

class OrderListItem(BaseModel):
    id: int
    status: OrderStatus
    total: float
    item_count: int
    created_at: datetime

class PaginatedOrders(BaseModel):
    items: List[OrderListItem]
    total: int
    page: int
    per_page: int
    pages: int

# Error Models
class ErrorResponse(BaseModel):
    detail: str
    code: Optional[str] = None

class ValidationErrorResponse(BaseModel):
    detail: List[dict]

# Endpoints
@app.post(
    "/products",
    response_model=ProductPublic,
    status_code=status.HTTP_201_CREATED,
    responses={
        201: {"description": "Product created successfully"},
        400: {"model": ErrorResponse, "description": "Invalid product data"}
    }
)
def create_product(product: ProductCreate):
    """Create a new product (public response - no internal data)."""
    db_product = {
        "id": 1,
        **product.model_dump(),
        "is_available": True,
        "created_at": datetime.now(),
        "profit_margin": (product.price - product.cost_price) / product.price
    }
    return db_product

@app.get(
    "/products",
    response_model=List[ProductPublic],
    response_model_exclude_none=True
)
def list_products(
    response: Response,
    skip: int = 0,
    limit: int = 20
):
    """List products with pagination headers."""
    products = list(products_db.values())[skip:skip + limit]
    response.headers["X-Total-Count"] = str(len(products_db))
    response.headers["X-Page-Size"] = str(limit)
    return products

@app.get(
    "/admin/products/{product_id}",
    response_model=ProductAdmin,
    responses={
        404: {"model": ErrorResponse}
    }
)
def admin_get_product(product_id: int):
    """Get product with internal data (admin only)."""
    if product_id not in products_db:
        raise HTTPException(
            status_code=404,
            detail="Product not found"
        )
    return products_db[product_id]

@app.post(
    "/orders",
    response_model=OrderResponse,
    status_code=status.HTTP_201_CREATED,
    responses={
        201: {"description": "Order created"},
        400: {"model": ErrorResponse, "description": "Invalid order"},
        422: {"model": ValidationErrorResponse}
    }
)
def create_order(order: OrderCreate):
    """Create a new order."""
    order_items = []
    total = 0

    for item in order.items:
        product = products_db.get(item.product_id)
        if not product:
            raise HTTPException(
                status_code=400,
                detail=f"Product {item.product_id} not found"
            )

        subtotal = product["price"] * item.quantity
        total += subtotal
        order_items.append({
            "product_id": item.product_id,
            "product_name": product["name"],
            "quantity": item.quantity,
            "unit_price": product["price"],
            "subtotal": subtotal
        })

    return {
        "id": 1,
        "status": OrderStatus.pending,
        "items": order_items,
        "total": total,
        "shipping_address": order.shipping_address,
        "created_at": datetime.now()
    }

@app.get(
    "/orders",
    response_model=PaginatedOrders
)
def list_orders(
    page: int = 1,
    per_page: int = 10,
    status_filter: Optional[OrderStatus] = None
):
    """List orders with pagination."""
    orders = list(orders_db.values())

    if status_filter:
        orders = [o for o in orders if o["status"] == status_filter]

    total = len(orders)
    pages = (total + per_page - 1) // per_page
    start = (page - 1) * per_page
    end = start + per_page

    return {
        "items": [
            {
                "id": o["id"],
                "status": o["status"],
                "total": o["total"],
                "item_count": len(o["items"]),
                "created_at": o["created_at"]
            }
            for o in orders[start:end]
        ],
        "total": total,
        "page": page,
        "per_page": per_page,
        "pages": pages
    }

@app.delete(
    "/orders/{order_id}",
    status_code=status.HTTP_204_NO_CONTENT,
    responses={
        204: {"description": "Order cancelled"},
        404: {"model": ErrorResponse},
        400: {"model": ErrorResponse, "description": "Cannot cancel order"}
    }
)
def cancel_order(order_id: int):
    """Cancel an order (no response body)."""
    if order_id not in orders_db:
        raise HTTPException(status_code=404, detail="Order not found")

    order = orders_db[order_id]
    if order["status"] not in [OrderStatus.pending, OrderStatus.confirmed]:
        raise HTTPException(
            status_code=400,
            detail="Cannot cancel order that has been shipped"
        )

    orders_db[order_id]["status"] = OrderStatus.cancelled
    return None

# In-memory storage
products_db = {}
orders_db = {}

Summary

FeatureUsage
response_modelDefine output structure
status_codeSet HTTP status code
response_model_excludeRemove specific fields
response_model_includeInclude only specific fields
responsesDocument multiple response types
Response objectSet headers dynamically
Response ClassUse Case
JSONResponseCustom JSON with headers
HTMLResponseHTML content
PlainTextResponsePlain text
RedirectResponseURL redirects
StreamingResponseLarge files, SSE
FileResponseFile downloads

Next Steps

In Part 5, we’ll explore Dependency Injection - FastAPI’s powerful system for sharing logic, database connections, authentication, and more across endpoints.

Series Navigation:

  • Part 1: Introduction and Setup
  • Part 2: Path and Query Parameters
  • Part 3: Request Bodies and Pydantic
  • Part 4: Response Models and Status Codes (You are here)
  • Part 5: Dependency Injection

Advertisement

MR

Moshiour Rahman

Software Architect & AI Engineer

Share:
MR

Moshiour Rahman

Software Architect & AI Engineer

Enterprise software architect with deep expertise in financial systems, distributed architecture, and AI-powered applications. Building large-scale systems at Fortune 500 companies. Specializing in LLM orchestration, multi-agent systems, and cloud-native solutions. I share battle-tested patterns from real enterprise projects.

Related Articles

Comments

Comments are powered by GitHub Discussions.

Configure Giscus at giscus.app to enable comments.