Skip to content

FastAPI Integration

TypedUUID integrates with FastAPI for building type-safe APIs.

Installation

pip install typed-uuid[fastapi]

This also installs Pydantic support.

Basic Usage

Path Parameters

Use path_param() for type-safe path parameters:

from fastapi import FastAPI
from typed_uuid import create_typed_uuid_class

app = FastAPI()

UserUUID = create_typed_uuid_class('User', 'user')

@app.get("/users/{user_id}")
async def get_user(user_id: UserUUID.path_param()):
    return {"user_id": str(user_id)}

Request Bodies

Use TypedUUID in Pydantic models for request bodies:

from pydantic import BaseModel

class CreateOrderRequest(BaseModel):
    user_id: UserUUID
    product_id: ProductUUID
    quantity: int

@app.post("/orders")
async def create_order(request: CreateOrderRequest):
    order_id = OrderUUID()
    return {
        "order_id": str(order_id),
        "user_id": str(request.user_id),
        "product_id": str(request.product_id)
    }

Response Models

Use TypedUUID in response models:

class UserResponse(BaseModel):
    id: UserUUID
    name: str
    email: str

@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: UserUUID.path_param()):
    return UserResponse(
        id=user_id,
        name="Alice",
        email="alice@example.com"
    )

Path Parameter Options

Custom Description

Add a description for OpenAPI documentation:

@app.get("/users/{user_id}")
async def get_user(
    user_id: UserUUID.path_param(description="The unique user identifier")
):
    return {"user_id": str(user_id)}

Generated Documentation

The path parameter automatically includes:

  • Example value: user-550e8400-e29b-41d4-a716-446655440000
  • Pattern validation
  • Type description

Full API Example

from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from typed_uuid import create_typed_uuid_class

app = FastAPI(title="User Management API")

# Create typed UUID classes
UserUUID = create_typed_uuid_class('User', 'user')
TeamUUID = create_typed_uuid_class('Team', 'team')

# In-memory storage (use a real database in production)
users_db = {}
teams_db = {}


# Models
class UserCreate(BaseModel):
    name: str
    email: str
    team_id: Optional[TeamUUID] = None


class UserResponse(BaseModel):
    id: UserUUID
    name: str
    email: str
    team_id: Optional[TeamUUID] = None


class TeamCreate(BaseModel):
    name: str


class TeamResponse(BaseModel):
    id: TeamUUID
    name: str
    member_ids: List[UserUUID]


# User endpoints
@app.post("/users", response_model=UserResponse)
async def create_user(request: UserCreate):
    user_id = UserUUID()
    user = UserResponse(
        id=user_id,
        name=request.name,
        email=request.email,
        team_id=request.team_id
    )
    users_db[str(user_id)] = user
    return user


@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: UserUUID.path_param(description="User ID")):
    user = users_db.get(str(user_id))
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    return user


@app.get("/users", response_model=List[UserResponse])
async def list_users():
    return list(users_db.values())


@app.delete("/users/{user_id}")
async def delete_user(user_id: UserUUID.path_param()):
    if str(user_id) not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    del users_db[str(user_id)]
    return {"deleted": True}


# Team endpoints
@app.post("/teams", response_model=TeamResponse)
async def create_team(request: TeamCreate):
    team_id = TeamUUID()
    team = TeamResponse(
        id=team_id,
        name=request.name,
        member_ids=[]
    )
    teams_db[str(team_id)] = team
    return team


@app.get("/teams/{team_id}", response_model=TeamResponse)
async def get_team(team_id: TeamUUID.path_param()):
    team = teams_db.get(str(team_id))
    if not team:
        raise HTTPException(status_code=404, detail="Team not found")
    return team

Query Parameters

For query parameters, use standard Pydantic validation:

from fastapi import Query
from typing import Optional

@app.get("/users")
async def list_users(
    team_id: Optional[str] = Query(None, description="Filter by team")
):
    if team_id:
        team_uuid = TeamUUID.from_string(team_id)
        # Filter users by team
    return list(users_db.values())

Error Handling

FastAPI automatically returns validation errors:

# Invalid UUID format returns 422 Unprocessable Entity
# GET /users/not-a-valid-uuid
# Response: {"detail": [{"loc": ["path", "user_id"], ...}]}

For custom error handling:

from fastapi import HTTPException
from typed_uuid import InvalidUUIDError

@app.get("/users/{user_id}")
async def get_user(user_id: UserUUID.path_param()):
    try:
        user = users_db.get(str(user_id))
        if not user:
            raise HTTPException(status_code=404, detail="User not found")
        return user
    except InvalidUUIDError:
        raise HTTPException(status_code=400, detail="Invalid user ID format")

Using Short IDs in URLs

Accept both standard and short format:

from typed_uuid import TypedUUID

@app.get("/users/{user_id}")
async def get_user(user_id: str):
    """Accept both standard and short UUID formats."""
    try:
        # Auto-detect format
        parsed_id = TypedUUID.parse(user_id)
        if not isinstance(parsed_id, UserUUID):
            raise HTTPException(status_code=400, detail="Expected user ID")
    except Exception:
        raise HTTPException(status_code=400, detail="Invalid user ID")

    return {"user_id": str(parsed_id)}

OpenAPI Schema

TypedUUID generates proper OpenAPI schemas:

paths:
  /users/{user_id}:
    get:
      parameters:
        - name: user_id
          in: path
          required: true
          schema:
            type: string
            pattern: "^[a-zA-Z0-9]+-[0-9a-f]{8}-..."
          example: "user-550e8400-e29b-41d4-a716-446655440000"

Best Practices

1. Consistent ID Types

# Define all ID types in one place
# app/ids.py
from typed_uuid import create_typed_uuid_class

UserUUID = create_typed_uuid_class('User', 'user')
OrderUUID = create_typed_uuid_class('Order', 'order')
ProductUUID = create_typed_uuid_class('Product', 'product')

# Import in your routes
from app.ids import UserUUID, OrderUUID, ProductUUID

2. Response Consistency

# Always use response models
@app.get("/users/{user_id}", response_model=UserResponse)
async def get_user(user_id: UserUUID.path_param()):
    # ...

# Not this (loses type information)
@app.get("/users/{user_id}")
async def get_user(user_id: UserUUID.path_param()):
    return {"id": str(user_id), ...}  # ID becomes plain string

3. Document Your API

@app.get(
    "/users/{user_id}",
    response_model=UserResponse,
    summary="Get a user by ID",
    description="Retrieve a user's details using their unique identifier."
)
async def get_user(
    user_id: UserUUID.path_param(description="The unique user identifier")
):
    """
    Get a user by their ID.

    - **user_id**: Must be a valid user UUID in format `user-{uuid}`
    """
    pass