Skip to main content

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.
I