Documentation Index
Fetch the complete documentation index at: https://docs.tasteful.heka.ai/llms.txt
Use this file to discover all available pages before exploring further.
Controller
Controllers are the HTTP interface layer of Tasteful flavors. They handle incoming HTTP requests, delegate business logic to services, and return appropriate responses. Controllers act as the entry point for all HTTP interactions with your application.
What is a Controller?
A Controller in Tasteful is a class that inherits from BaseController and defines HTTP endpoints using route decorators. Controllers are responsible for:
- HTTP Request Handling: Processing incoming requests and extracting data
- Route Definition: Mapping URLs to specific handler methods
- Response Formatting: Returning properly formatted HTTP responses
- Service Orchestration: Delegating business logic to services
- Input Validation: Basic request validation (detailed validation happens in services)
Controllers should be thin - they handle HTTP concerns but delegate all business logic to services.
Core Concepts
BaseController Foundation
All controllers inherit from BaseController, which provides:
from tasteful.base_flavor import BaseController
class UserController(BaseController):
def __init__(self, user_service: UserService):
super().__init__(prefix="/users", tags=["users"])
self.user_service = user_service
Key features of BaseController:
- Automatic Router Creation: Creates a FastAPI
APIRouter instance
- URL Prefixing: All routes are prefixed with the specified path
- Tag Organization: Groups endpoints for API documentation
- Endpoint Registration: Automatically registers decorated methods as routes
Route Decorators
Tasteful provides HTTP method decorators that mirror FastAPI’s functionality:
from tasteful.decorators.controller import Get, Post, Put, Delete, Patch, Head, Options, Trace
class UserController(BaseController):
def __init__(self, user_service: UserService):
super().__init__(prefix="/users", tags=["users"])
self.user_service = user_service
@Get("/")
async def list_users(self):
return self.user_service.list_users()
@Post("/")
async def create_user(self, user_data: dict):
return self.user_service.create_user(user_data)
@Get("/{user_id}")
async def get_user(self, user_id: int):
return self.user_service.get_user(user_id)
@Put("/{user_id}")
async def update_user(self, user_id: int, user_data: dict):
return self.user_service.update_user(user_id, user_data)
@Delete("/{user_id}")
async def delete_user(self, user_id: int):
return self.user_service.delete_user(user_id)
Implementation
Basic Controller Implementation
Here’s a complete example of a controller implementation:
from tasteful.base_flavor import BaseController
from tasteful.decorators.controller import Get, Post, Put, Delete
from typing import Dict, List
class ProductController(BaseController):
"""Controller for product management endpoints."""
def __init__(self, product_service: ProductService):
super().__init__(prefix="/products", tags=["products"])
self.product_service = product_service
@Get("/")
async def list_products(self, skip: int = 0, limit: int = 100) -> List[Dict]:
"""List all products with pagination."""
return self.product_service.list_products(skip=skip, limit=limit)
@Get("/{product_id}")
async def get_product(self, product_id: int) -> Dict:
"""Get a specific product by ID."""
return self.product_service.get_product(product_id)
@Post("/")
async def create_product(self, product_data: Dict) -> Dict:
"""Create a new product."""
return self.product_service.create_product(product_data)
@Put("/{product_id}")
async def update_product(self, product_id: int, product_data: Dict) -> Dict:
"""Update an existing product."""
return self.product_service.update_product(product_id, product_data)
@Delete("/{product_id}")
async def delete_product(self, product_id: int) -> Dict:
"""Delete a product."""
return self.product_service.delete_product(product_id)
Advanced Patterns
Multiple Service Dependencies
Controllers can depend on multiple services:
class OrderController(BaseController):
def __init__(
self,
order_service: OrderService,
payment_service: PaymentService,
inventory_service: InventoryService
):
super().__init__(prefix="/orders", tags=["orders"])
self.order_service = order_service
self.payment_service = payment_service
self.inventory_service = inventory_service
@Post("/")
async def create_order(self, order_data: Dict) -> Dict:
"""Create a new order with payment and inventory checks."""
# Controller orchestrates multiple services
inventory_check = self.inventory_service.check_availability(order_data["items"])
if not inventory_check["available"]:
return {"error": "Items not available"}
order = self.order_service.create_order(order_data)
payment_result = self.payment_service.process_payment(order["id"], order_data["payment"])
if payment_result["success"]:
return order
else:
self.order_service.cancel_order(order["id"])
return {"error": "Payment failed"}
Custom Route Parameters
You can pass additional FastAPI parameters to route decorators:
from fastapi import HTTPException, status
class UserController(BaseController):
def __init__(self, user_service: UserService):
super().__init__(prefix="/users", tags=["users"])
self.user_service = user_service
@Get(
"/{user_id}",
response_model=UserResponse,
status_code=status.HTTP_200_OK,
summary="Get user by ID",
description="Retrieve a specific user by their unique identifier"
)
async def get_user(self, user_id: int) -> UserResponse:
user = self.user_service.get_user(user_id)
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
return user
Integration with Other Components
Controller-Service Relationship
Controllers should always delegate business logic to services:
# ✅ Good: Thin controller, business logic in service
class UserController(BaseController):
def __init__(self, user_service: UserService):
super().__init__(prefix="/users", tags=["users"])
self.user_service = user_service
@Post("/")
async def create_user(self, user_data: Dict) -> Dict:
# Controller handles HTTP concerns, service handles business logic
return self.user_service.create_user(user_data)
# ❌ Bad: Business logic in controller
class UserController(BaseController):
def __init__(self, user_repository: UserRepository):
super().__init__(prefix="/users", tags=["users"])
self.user_repository = user_repository
@Post("/")
async def create_user(self, user_data: Dict) -> Dict:
# Business logic should be in service, not controller
if not user_data.get("email"):
return {"error": "Email required"}
if self.user_repository.email_exists(user_data["email"]):
return {"error": "Email already exists"}
user = self.user_repository.create(user_data)
return user
Dependency Injection
Controllers receive their dependencies through constructor injection, managed automatically by Tasteful’s dependency injection system:
class UserController(BaseController):
def __init__(
self,
user_service: UserService, # ← Automatically injected
auth_service: AuthService # ← Automatically injected
):
super().__init__(prefix="/users", tags=["users"])
self.user_service = user_service
self.auth_service = auth_service
# In your flavor definition
class UserFlavor(BaseFlavor):
def __init__(self):
super().__init__(
controller=UserController,
services=[UserService, AuthService], # ← Services registered for injection
repositories=[UserRepository],
config=UserConfig
)
Best Practices
Do’s
- Keep controllers thin - Delegate all business logic to services
- Use meaningful HTTP status codes - Return appropriate status codes for different scenarios
- Handle errors gracefully - Convert service exceptions to appropriate HTTP responses
- Use type hints - Provide clear type annotations for better documentation and IDE support
- Follow RESTful conventions - Use standard HTTP methods and URL patterns
- Document endpoints - Use docstrings and FastAPI parameters for API documentation
class UserController(BaseController):
def __init__(self, user_service: UserService):
super().__init__(prefix="/users", tags=["users"])
self.user_service = user_service
@Get("/{user_id}", response_model=UserResponse)
async def get_user(self, user_id: int) -> UserResponse:
"""Get a user by their ID."""
try:
return self.user_service.get_user(user_id)
except UserNotFoundError:
raise HTTPException(status_code=404, detail="User not found")
except Exception as e:
raise HTTPException(status_code=500, detail="Internal server error")
Don’ts
- Don’t put business logic in controllers - Controllers should only handle HTTP concerns
- Don’t access repositories directly - Always go through services
- Don’t handle complex validation - Let services handle detailed validation
- Don’t manage transactions - Leave transaction management to services
- Don’t hardcode responses - Use proper response models and status codes
# ❌ Bad: Business logic in controller
class UserController(BaseController):
@Post("/")
async def create_user(self, user_data: Dict):
# Don't do validation in controller
if len(user_data.get("password", "")) < 8:
return {"error": "Password too short"}
# Don't access repository directly
user = self.user_repository.create(user_data)
# Don't handle complex business logic
if user.is_premium:
self.send_welcome_email(user)
self.create_premium_benefits(user)
return user
# ✅ Good: Thin controller
class UserController(BaseController):
@Post("/")
async def create_user(self, user_data: Dict):
return self.user_service.create_user(user_data)
Common Patterns
CRUD Operations
Most controllers follow standard CRUD patterns:
class ResourceController(BaseController):
def __init__(self, resource_service: ResourceService):
super().__init__(prefix="/resources", tags=["resources"])
self.resource_service = resource_service
@Get("/")
async def list_resources(self, skip: int = 0, limit: int = 100):
"""List resources with pagination."""
return self.resource_service.list_resources(skip=skip, limit=limit)
@Get("/{resource_id}")
async def get_resource(self, resource_id: int):
"""Get a specific resource."""
return self.resource_service.get_resource(resource_id)
@Post("/")
async def create_resource(self, resource_data: Dict):
"""Create a new resource."""
return self.resource_service.create_resource(resource_data)
@Put("/{resource_id}")
async def update_resource(self, resource_id: int, resource_data: Dict):
"""Update an existing resource."""
return self.resource_service.update_resource(resource_id, resource_data)
@Delete("/{resource_id}")
async def delete_resource(self, resource_id: int):
"""Delete a resource."""
return self.resource_service.delete_resource(resource_id)
Nested Resources
Handle nested resource relationships:
class CommentController(BaseController):
def __init__(self, comment_service: CommentService):
super().__init__(prefix="/posts", tags=["comments"])
self.comment_service = comment_service
@Get("/{post_id}/comments")
async def list_post_comments(self, post_id: int):
"""List all comments for a specific post."""
return self.comment_service.list_comments_for_post(post_id)
@Post("/{post_id}/comments")
async def create_comment(self, post_id: int, comment_data: Dict):
"""Create a new comment on a post."""
return self.comment_service.create_comment(post_id, comment_data)
@Get("/{post_id}/comments/{comment_id}")
async def get_comment(self, post_id: int, comment_id: int):
"""Get a specific comment on a post."""
return self.comment_service.get_comment(post_id, comment_id)
Testing
Controllers are easily testable thanks to dependency injection:
import pytest
from unittest.mock import Mock
from fastapi.testclient import TestClient
def test_user_controller_get_user():
# Arrange
mock_user_service = Mock(spec=UserService)
mock_user_service.get_user.return_value = {"id": 1, "name": "John Doe"}
controller = UserController(user_service=mock_user_service)
client = TestClient(controller.router)
# Act
response = client.get("/1")
# Assert
assert response.status_code == 200
assert response.json() == {"id": 1, "name": "John Doe"}
mock_user_service.get_user.assert_called_once_with(1)
def test_user_controller_create_user():
# Arrange
mock_user_service = Mock(spec=UserService)
mock_user_service.create_user.return_value = {"id": 1, "name": "Jane Doe"}
controller = UserController(user_service=mock_user_service)
client = TestClient(controller.router)
user_data = {"name": "Jane Doe", "email": "jane@example.com"}
# Act
response = client.post("/", json=user_data)
# Assert
assert response.status_code == 200
assert response.json() == {"id": 1, "name": "Jane Doe"}
mock_user_service.create_user.assert_called_once_with(user_data)
Controllers provide the HTTP interface for your Tasteful flavors, handling requests and responses while keeping business logic properly separated in services. They work seamlessly with Tasteful’s dependency injection system to create clean, testable, and maintainable web APIs.