Files
ponytail/examples/api-endpoint.md
T

1.7 KiB

API Endpoint

Task: "Add an endpoint that returns a user by id."

Without Ponytail

app/
├── controllers/user_controller.py
├── services/user_service.py
├── repositories/user_repository.py
├── schemas/user_schemas.py
└── exceptions/user_exceptions.py
# schemas/user_schemas.py
class UserResponseDTO(BaseModel):
    id: int
    name: str
    email: str

# repositories/user_repository.py
class UserRepository:
    def __init__(self, db: Session):
        self.db = db
    def find_by_id(self, user_id: int) -> User | None:
        return self.db.get(User, user_id)

# services/user_service.py
class UserService:
    def __init__(self, repo: UserRepository):
        self.repo = repo
    def get_user(self, user_id: int) -> User:
        user = self.repo.find_by_id(user_id)
        if user is None:
            raise UserNotFoundError(user_id)
        return user

# controllers/user_controller.py
@router.get("/users/{user_id}", response_model=UserResponseDTO)
def get_user(user_id: int, service: UserService = Depends(get_user_service)):
    try:
        return service.get_user(user_id)
    except UserNotFoundError:
        raise HTTPException(status_code=404, detail="User not found")

Five files, three classes, a custom exception, and a dependency-injection chain — wrapping one database call.

With Ponytail

# ponytail: it's one query
@app.get("/users/{user_id}")
def get_user(user_id: int, db: Session = Depends(get_db)):
    user = db.get(User, user_id)
    if not user:
        raise HTTPException(404)
    return user

5 files → 5 lines. Layers earn their place when there are two implementations, not before. Add the service layer when a second caller shows up — if it ever does.