Skip to content

๐Ÿ”Œ MVC Controllersยถ

Neuroglia's MVC system provides powerful class-based API development using Mario's Pizzeria as an example, demonstrating real-world controller patterns with automatic discovery, dependency injection, and comprehensive API design.

๐ŸŽฏ What You'll Learnยถ

  • Pizza Order Management: OrdersController for handling customer orders
  • Menu Administration: MenuController for pizza and topping management
  • Kitchen Operations: KitchenController for order preparation workflow
  • Authentication & Authorization: OAuth integration for staff and customer access
  • Error Handling: Comprehensive error responses and validation
  • API Documentation: Automatic OpenAPI generation with pizzeria examples

๐Ÿ—๏ธ Controller Foundationยถ

Pizza Order Controllerยถ

The main controller for customer interactions at Mario's Pizzeria:

from neuroglia.mvc.controller_base import ControllerBase
from neuroglia.dependency_injection.service_provider import ServiceProviderBase
from neuroglia.mapping.mapper import Mapper
from neuroglia.mediation.mediator import Mediator
from classy_fastapi.decorators import get, post, put, delete
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer
from typing import List, Optional
from datetime import date

class OrdersController(ControllerBase):
    """Controller for managing pizza orders at Mario's Pizzeria"""

    def __init__(self, 
                 service_provider: ServiceProviderBase,
                 mapper: Mapper,
                 mediator: Mediator):
        super().__init__(service_provider, mapper, mediator)
        self.security = HTTPBearer(auto_error=False)

    @get("/", 
         response_model=List[OrderDto], 
         summary="Get customer orders",
         description="Retrieve orders for authenticated customer")
    async def get_my_orders(self, 
                            token: str = Depends(HTTPBearer()),
                            limit: int = 10) -> List[OrderDto]:
        """Get orders for authenticated customer"""
        try:
            # Validate customer token and get customer info
            customer_info = await self._validate_customer_token(token.credentials)

            # Query customer's orders
            query = GetOrdersByCustomerQuery(
                customer_phone=customer_info.phone,
                limit=limit
            )
            result = await self.mediator.execute_async(query)

            return self.process(result)

        except Exception as e:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid authentication token"
            )

    @get("/{order_id}", 
         response_model=OrderDto,
         summary="Get specific order",
         description="Get details of a specific pizza order")
    async def get_order(self, 
                        order_id: str,
                        token: str = Depends(HTTPBearer())) -> OrderDto:
        """Get specific order details"""
        # Validate customer access to this order
        customer_info = await self._validate_customer_token(token.credentials)

        query = GetOrderByIdQuery(
            order_id=order_id,
            customer_phone=customer_info.phone  # Ensure customer owns order
        )
        result = await self.mediator.execute_async(query)

        return self.process(result)

    @post("/", 
          response_model=OrderDto, 
          status_code=201,
          summary="Place pizza order",
          description="Place a new pizza order with customer details and pizza selection")
    async def place_order(self, 
                          order_request: PlaceOrderDto,
                          token: Optional[str] = Depends(HTTPBearer(auto_error=False))) -> OrderDto:
        """Place a new pizza order"""
        try:
            # If token provided, use customer info; otherwise use order details
            customer_info = None
            if token:
                customer_info = await self._validate_customer_token(token.credentials)

            # Create place order command
            command = PlaceOrderCommand(
                customer_name=customer_info.name if customer_info else order_request.customer_name,
                customer_phone=customer_info.phone if customer_info else order_request.customer_phone,
                customer_address=order_request.customer_address,
                pizzas=order_request.pizzas,
                payment_method=order_request.payment_method,
                special_instructions=order_request.special_instructions
            )

            result = await self.mediator.execute_async(command)
            return self.process(result)

        except ValidationError as e:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=f"Invalid order data: {str(e)}"
            )
        except Exception as e:
            raise HTTPException(
                status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
                detail="Failed to place order. Please try again."
            )

    @put("/{order_id}/cancel", 
         response_model=OrderDto,
         summary="Cancel order",
         description="Cancel a pizza order if it hasn't started preparation")
    async def cancel_order(self, 
                           order_id: str,
                           cancellation_request: CancelOrderDto,
                           token: str = Depends(HTTPBearer())) -> OrderDto:
        """Cancel an existing order"""
        customer_info = await self._validate_customer_token(token.credentials)

        command = CancelOrderCommand(
            order_id=order_id,
            customer_phone=customer_info.phone,
            cancellation_reason=cancellation_request.reason
        )

        result = await self.mediator.execute_async(command)
        return self.process(result)

    @get("/{order_id}/status",
         response_model=OrderStatusDto,
         summary="Get order status",
         description="Get current status and estimated ready time for order")
    async def get_order_status(self, 
                               order_id: str,
                               token: str = Depends(HTTPBearer())) -> OrderStatusDto:
        """Get order status and tracking information"""
        customer_info = await self._validate_customer_token(token.credentials)

        query = GetOrderStatusQuery(
            order_id=order_id,
            customer_phone=customer_info.phone
        )

        result = await self.mediator.execute_async(query)
        return self.process(result)

    async def _validate_customer_token(self, token: str) -> CustomerInfo:
        """Validate customer authentication token"""
        # In production, this would validate JWT token
        # For demo purposes, we'll use a simple validation
        query = ValidateCustomerTokenQuery(token=token)
        result = await self.mediator.execute_async(query)

        if not result.is_success:
            raise HTTPException(
                status_code=status.HTTP_401_UNAUTHORIZED,
                detail="Invalid or expired token"
            )

        return result.data
class MenuController(ControllerBase):
    """Controller for managing Mario's Pizzeria menu"""

    def __init__(self, 
                 service_provider: ServiceProviderBase,
                 mapper: Mapper,
                 mediator: Mediator):
        super().__init__(service_provider, mapper, mediator)

    @get("/pizzas", 
         response_model=List[PizzaDto],
         summary="Get pizza menu",
         description="Get all available pizzas organized by category")
    async def get_menu(self, 
                       category: Optional[str] = Query(None, description="Filter by pizza category"),
                       available_only: bool = Query(True, description="Show only available pizzas")) -> List[PizzaDto]:
        """Get pizza menu with optional filtering"""
        query = GetMenuQuery(
            category=category,
            available_only=available_only
        )
        result = await self.mediator.execute_async(query)
        return self.process(result)

    @get("/pizzas/{pizza_id}", 
         response_model=PizzaDto,
         summary="Get pizza details",
         description="Get detailed information about a specific pizza")
    async def get_pizza(self, pizza_id: str) -> PizzaDto:
        """Get specific pizza details"""
        query = GetPizzaByIdQuery(pizza_id=pizza_id)
        result = await self.mediator.execute_async(query)
        return self.process(result)

    @get("/categories",
         response_model=List[str],
         summary="Get pizza categories",
         description="Get all available pizza categories")
    async def get_categories(self) -> List[str]:
        """Get all pizza categories"""
        query = GetPizzaCategoriesQuery()
        result = await self.mediator.execute_async(query)
        return self.process(result)

    @get("/toppings",
         response_model=List[ToppingDto],
         summary="Get available toppings",
         description="Get all available pizza toppings with prices")
    async def get_toppings(self,
                           vegetarian_only: bool = Query(False, description="Show only vegetarian toppings")) -> List[ToppingDto]:
        """Get available toppings"""
        query = GetToppingsQuery(vegetarian_only=vegetarian_only)
        result = await self.mediator.execute_async(query)
        return self.process(result)

    # Admin endpoints (require staff authentication)
    @post("/pizzas", 
          response_model=PizzaDto, 
          status_code=201,
          summary="Add new pizza (Staff Only)",
          description="Add a new pizza to the menu")
    async def add_pizza(self, 
                        pizza_request: CreatePizzaDto,
                        staff_token: str = Depends(HTTPBearer())) -> PizzaDto:
        """Add new pizza to menu (staff only)"""
        await self._validate_staff_token(staff_token.credentials, required_role="manager")

        command = CreatePizzaCommand(
            name=pizza_request.name,
            description=pizza_request.description,
            category=pizza_request.category,
            base_price=pizza_request.base_price,
            available_toppings=pizza_request.available_toppings,
            preparation_time_minutes=pizza_request.preparation_time_minutes,
            is_seasonal=pizza_request.is_seasonal
        )

        result = await self.mediator.execute_async(command)
        return self.process(result)

    @put("/pizzas/{pizza_id}/availability", 
         response_model=PizzaDto,
         summary="Update pizza availability (Staff Only)",
         description="Mark pizza as available or sold out")
    async def update_pizza_availability(self, 
                                        pizza_id: str,
                                        availability_request: UpdateAvailabilityDto,
                                        staff_token: str = Depends(HTTPBearer())) -> PizzaDto:
        """Update pizza availability"""
        await self._validate_staff_token(staff_token.credentials, required_role="staff")

        command = UpdatePizzaAvailabilityCommand(
            pizza_id=pizza_id,
            is_available=availability_request.is_available,
            reason=availability_request.reason
        )

        result = await self.mediator.execute_async(command)
        return self.process(result)

Kitchen Operations Controllerยถ

class KitchenController(ControllerBase):
    """Controller for kitchen operations and order management"""

    def __init__(self, 
                 service_provider: ServiceProviderBase,
                 mapper: Mapper,
                 mediator: Mediator):
        super().__init__(service_provider, mapper, mediator)

    @get("/queue", 
         response_model=List[KitchenOrderDto],
         summary="Get kitchen queue",
         description="Get orders in kitchen queue ordered by priority")
    async def get_kitchen_queue(self,
                                staff_token: str = Depends(HTTPBearer())) -> List[KitchenOrderDto]:
        """Get orders in kitchen preparation queue"""
        await self._validate_staff_token(staff_token.credentials, required_role="kitchen")

        query = GetKitchenQueueQuery(
            statuses=["received", "preparing", "cooking"]
        )
        result = await self.mediator.execute_async(query)
        return self.process(result)

    @put("/orders/{order_id}/status", 
         response_model=OrderDto,
         summary="Update order status",
         description="Update order status in kitchen workflow")
    async def update_order_status(self, 
                                  order_id: str,
                                  status_update: UpdateOrderStatusDto,
                                  staff_token: str = Depends(HTTPBearer())) -> OrderDto:
        """Update order status (kitchen staff only)"""
        staff_info = await self._validate_staff_token(staff_token.credentials, required_role="kitchen")

        command = UpdateOrderStatusCommand(
            order_id=order_id,
            new_status=status_update.status,
            updated_by=staff_info.staff_id,
            notes=status_update.notes,
            estimated_ready_time=status_update.estimated_ready_time
        )

        result = await self.mediator.execute_async(command)
        return self.process(result)

    @post("/orders/{order_id}/pizzas/{pizza_index}/start", 
          response_model=OrderDto,
          summary="Start pizza preparation",
          description="Mark pizza as started in preparation")
    async def start_pizza(self, 
                          order_id: str,
                          pizza_index: int,
                          staff_token: str = Depends(HTTPBearer())) -> OrderDto:
        """Start pizza preparation"""
        staff_info = await self._validate_staff_token(staff_token.credentials, required_role="kitchen")

        command = StartPizzaPreparationCommand(
            order_id=order_id,
            pizza_index=pizza_index,
            chef_id=staff_info.staff_id
        )

        result = await self.mediator.execute_async(command)
        return self.process(result)

    @post("/orders/{order_id}/pizzas/{pizza_index}/complete", 
          response_model=OrderDto,
          summary="Complete pizza preparation",
          description="Mark pizza as completed")
    async def complete_pizza(self, 
                             order_id: str,
                             pizza_index: int,
                             completion_request: CompletePizzaDto,
                             staff_token: str = Depends(HTTPBearer())) -> OrderDto:
        """Complete pizza preparation"""
        staff_info = await self._validate_staff_token(staff_token.credentials, required_role="kitchen")

        command = CompletePizzaPreparationCommand(
            order_id=order_id,
            pizza_index=pizza_index,
            chef_id=staff_info.staff_id,
            quality_notes=completion_request.quality_notes
        )

        result = await self.mediator.execute_async(command)
        return self.process(result)

    @get("/performance", 
         response_model=KitchenPerformanceDto,
         summary="Get kitchen performance metrics",
         description="Get kitchen performance analytics")
    async def get_performance_metrics(self,
                                      start_date: date = Query(description="Start date for metrics"),
                                      end_date: date = Query(description="End date for metrics"),
                                      staff_token: str = Depends(HTTPBearer())) -> KitchenPerformanceDto:
        """Get kitchen performance metrics"""
        await self._validate_staff_token(staff_token.credentials, required_role="manager")

        query = GetKitchenPerformanceQuery(
            start_date=start_date,
            end_date=end_date
        )
        result = await self.mediator.execute_async(query)
        return self.process(result)

    async def _validate_staff_token(self, token: str, required_role: str) -> StaffInfo:
        """Validate staff authentication and role"""
        query = ValidateStaffTokenQuery(
            token=token,
            required_role=required_role
        )
        result = await self.mediator.execute_async(query)

        if not result.is_success:
            raise HTTPException(
                status_code=status.HTTP_403_FORBIDDEN,
                detail=f"Insufficient permissions. Required role: {required_role}"
            )

        return result.data
class ReportsController(ControllerBase):
    """Controller for pizzeria analytics and reporting"""

    @get("/orders", 
         response_model=List[OrderReportDto],
         summary="Get order reports",
         description="Get filtered order data for reporting")
    async def get_order_reports(self,
                                start_date: date = Query(description="Report start date"),
                                end_date: date = Query(description="Report end date"),
                                customer_phone: Optional[str] = Query(None, description="Filter by customer"),
                                status: Optional[str] = Query(None, description="Filter by order status"),
                                min_amount: Optional[float] = Query(None, ge=0, description="Minimum order amount"),
                                max_amount: Optional[float] = Query(None, ge=0, description="Maximum order amount"),
                                limit: int = Query(100, ge=1, le=1000, description="Maximum results to return"),
                                offset: int = Query(0, ge=0, description="Number of results to skip"),
                                staff_token: str = Depends(HTTPBearer())) -> List[OrderReportDto]:
        """Get order reports with advanced filtering"""
        await self._validate_staff_token(staff_token.credentials, required_role="manager")

        query = GetOrderReportsQuery(
            start_date=start_date,
            end_date=end_date,
            customer_phone=customer_phone,
            status=status,
            min_amount=min_amount,
            max_amount=max_amount,
            limit=limit,
            offset=offset
        )

        result = await self.mediator.execute_async(query)
        return self.process(result)

    @get("/revenue", 
         response_model=RevenueReportDto,
         summary="Get revenue analytics",
         description="Get revenue breakdown and analytics")
    async def get_revenue_report(self,
                                 period: str = Query("daily", regex="^(daily|weekly|monthly)$"),
                                 start_date: date = Query(description="Analysis start date"),
                                 end_date: date = Query(description="Analysis end date"),
                                 staff_token: str = Depends(HTTPBearer())) -> RevenueReportDto:
        """Get revenue analytics by period"""
        await self._validate_staff_token(staff_token.credentials, required_role="manager")

        query = GetRevenueAnalyticsQuery(
            period=period,
            start_date=start_date,
            end_date=end_date
        )

        result = await self.mediator.execute_async(query)
        return self.process(result)

Request Validation and DTOsยถ

Comprehensive validation for pizzeria data: """Get users with filtering and pagination"""

    query = GetUsersQuery(
        department=department,
        active_only=active_only,
        page=page,
        page_size=page_size
    )

    result = await self.mediator.execute_async(query)
    return self.process(result)

```

Request Body Validationยถ

Use Pydantic models for request validation: python from pydantic import BaseModel, Field, EmailStr from typing import Optional class CreateUserDto(BaseModel): email: EmailStr = Field(..., description="User's email address") first_name: str = Field(..., min_length=1, max_length=50, description="First name") last_name: str = Field(..., min_length=1, max_length=50, description="Last name") department: Optional[str] = Field(None, max_length=100, description="Department") class Config: schema_extra = { "example": { "email": "john.doe@company.com", "first_name": "John", "last_name": "Doe", "department": "Engineering" } } class UsersController(ControllerBase): @post("/", response_model=UserDto, status_code=status.HTTP_201_CREATED) async def create_user(self, create_user_dto: CreateUserDto) -> UserDto: """Create a new user""" command = self.mapper.map(create_user_dto, CreateUserCommand) result = await self.mediator.execute_async(command) return self.process(result)

File Uploadsยถ

Handle file uploads:

from fastapi import UploadFile, File

class UsersController(ControllerBase):

    @post("/{user_id}/avatar", response_model=UserDto)
    async def upload_avatar(self, 
                           user_id: str,
                           file: UploadFile = File(..., description="Avatar image")) -> UserDto:
        """Upload user avatar"""

        # Validate file type
        if not file.content_type.startswith('image/'):
            return self.bad_request("File must be an image")

        # Create command
        command = UploadUserAvatarCommand(
            user_id=user_id,
            file_name=file.filename,
            file_content=await file.read(),
            content_type=file.content_type
        )

        result = await self.mediator.execute_async(command)
        return self.process(result)

Response Headersยถ

Set custom response headers:

from fastapi import Response

class UsersController(ControllerBase):

    @get("/{user_id}/export", response_class=Response)
    async def export_user_data(self, user_id: str, response: Response):
        """Export user data as CSV"""

        query = ExportUserDataQuery(user_id=user_id)
        result = await self.mediator.execute_async(query)

        if not result.is_success:
            return self.process(result)

        # Set CSV headers
        response.headers["Content-Type"] = "text/csv"
        response.headers["Content-Disposition"] = f"attachment; filename=user_{user_id}.csv"

        return result.data

๐ŸŽช Controller Configurationยถ

Custom Routingยถ

Customize controller routing:

class UsersController(ControllerBase):
    def __init__(self, service_provider, mapper, mediator):
        super().__init__(service_provider, mapper, mediator)

        # Custom prefix and tags
        self.router.prefix = "/users"
        self.router.tags = ["User Management"]

        # Add custom middleware to this controller
        self.router.middleware("http")(self.auth_middleware)

    async def auth_middleware(self, request, call_next):
        """Custom authentication middleware for this controller"""
        # Authentication logic
        response = await call_next(request)
        return response

Nested Controllersยถ

Create hierarchical resource structures:

class UserAccountsController(ControllerBase):
    """Handles user account operations"""

    def __init__(self, service_provider, mapper, mediator):
        super().__init__(service_provider, mapper, mediator)
        self.router.prefix = "/users/{user_id}/accounts"

    @get("/", response_model=List[AccountDto])
    async def get_user_accounts(self, user_id: str) -> List[AccountDto]:
        """Get all accounts for a user"""
        query = GetUserAccountsQuery(user_id=user_id)
        result = await self.mediator.execute_async(query)
        return self.process(result)

    @post("/", response_model=AccountDto, status_code=status.HTTP_201_CREATED)
    async def create_account(self, user_id: str, create_account_dto: CreateAccountDto) -> AccountDto:
        """Create a new account for a user"""
        command = self.mapper.map(create_account_dto, CreateAccountCommand)
        command.user_id = user_id
        result = await self.mediator.execute_async(command)
        return self.process(result)

๐Ÿ›ก๏ธ Error Handlingยถ

Built-in Error Responsesยถ

Controllers include standard error responses:

class UsersController(ControllerBase):

    @get("/{user_id}", 
         response_model=UserDto,
         responses=ControllerBase.error_responses)  # Adds 400, 404, 500 responses
    async def get_user(self, user_id: str) -> UserDto:
        """Get user by ID"""
        query = GetUserByIdQuery(user_id=user_id)
        result = await self.mediator.execute_async(query)
        return self.process(result)  # Automatically handles error responses

Custom Error Handlingยถ

Add custom error handling:

from fastapi import HTTPException

class UsersController(ControllerBase):

    @post("/", response_model=UserDto, status_code=status.HTTP_201_CREATED)
    async def create_user(self, create_user_dto: CreateUserDto) -> UserDto:
        """Create a new user"""
        try:
            command = self.mapper.map(create_user_dto, CreateUserCommand)
            result = await self.mediator.execute_async(command)
            return self.process(result)

        except EmailAlreadyExistsException:
            raise HTTPException(
                status_code=status.HTTP_409_CONFLICT,
                detail="A user with this email already exists"
            )
        except ValidationException as ex:
            raise HTTPException(
                status_code=status.HTTP_400_BAD_REQUEST,
                detail=str(ex)
            )

Global Error Handlingยถ

Use middleware for global error handling:

from neuroglia.hosting.web import ExceptionHandlingMiddleware

# In main.py
app.add_middleware(ExceptionHandlingMiddleware, service_provider=app.services)

๐Ÿ” Authentication & Authorizationยถ

Dependency Injection for Authยถ

Inject authentication services:

from fastapi import Depends
from neuroglia.security import IAuthService, AuthUser

class UsersController(ControllerBase):

    def __init__(self, 
                 service_provider: ServiceProviderBase,
                 mapper: Mapper,
                 mediator: Mediator,
                 auth_service: IAuthService):
        super().__init__(service_provider, mapper, mediator)
        self.auth_service = auth_service

    @get("/profile", response_model=UserDto)
    async def get_current_user(self, 
                              current_user: AuthUser = Depends(auth_service.get_current_user)) -> UserDto:
        """Get current user's profile"""
        query = GetUserByIdQuery(user_id=current_user.user_id)
        result = await self.mediator.execute_async(query)
        return self.process(result)

Role-based Authorizationยถ

Implement role-based access control:

from neuroglia.security import require_role

class UsersController(ControllerBase):

    @get("/", response_model=List[UserDto])
    @require_role("admin")  # Custom decorator
    async def get_all_users(self) -> List[UserDto]:
        """Get all users (admin only)"""
        query = GetAllUsersQuery()
        result = await self.mediator.execute_async(query)
        return self.process(result)

    @delete("/{user_id}")
    @require_role(["admin", "manager"])  # Multiple roles
    async def delete_user(self, user_id: str):
        """Delete a user (admin or manager only)"""
        command = DeleteUserCommand(user_id=user_id)
        result = await self.mediator.execute_async(command)
        self.process(result)

๐Ÿ“Š Response Processingยถ

The process Methodยถ

The process method handles OperationResult objects automatically:

# OperationResult with data
result = OperationResult.success(user_dto)
return self.process(result)  # Returns user_dto with 200 status

# OperationResult with error
result = OperationResult.not_found("User not found")
return self.process(result)  # Raises HTTPException with 404 status

# OperationResult created
result = OperationResult.created(user_dto)
return self.process(result)  # Returns user_dto with 201 status

Custom Response Processingยถ

Override response processing for special cases:

class UsersController(ControllerBase):

    @get("/{user_id}", response_model=UserDto)
    async def get_user(self, user_id: str) -> UserDto:
        """Get user by ID"""
        query = GetUserByIdQuery(user_id=user_id)
        result = await self.mediator.execute_async(query)

        # Custom processing
        if not result.is_success:
            if result.status_code == 404:
                # Log the attempt
                self.logger.warning(f"Attempt to access non-existent user: {user_id}")
            return self.process(result)

        # Add custom headers for successful responses
        response = self.process(result)
        # Custom logic here
        return response

๐Ÿงช Testing Controllersยถ

Unit Testingยถ

Test controllers with mocked dependencies:

import pytest
from unittest.mock import Mock, AsyncMock

@pytest.mark.asyncio
async def test_get_user_success():
    # Arrange
    mock_mediator = Mock()
    mock_mediator.execute_async = AsyncMock(return_value=OperationResult.success(test_user_dto))

    controller = UsersController(
        service_provider=mock_service_provider,
        mapper=mock_mapper,
        mediator=mock_mediator
    )

    # Act
    result = await controller.get_user("user123")

    # Assert
    assert result == test_user_dto
    mock_mediator.execute_async.assert_called_once()

Integration Testingยถ

Test controllers with TestClient:

from fastapi.testclient import TestClient

def test_create_user_integration():
    # Arrange
    client = TestClient(app)
    user_data = {
        "email": "test@example.com",
        "first_name": "John",
        "last_name": "Doe"
    }

    # Act
    response = client.post("/api/v1/users", json=user_data)

    # Assert
    assert response.status_code == 201

    created_user = response.json()
    assert created_user["email"] == user_data["email"]
    assert "id" in created_user

API Testingยถ

Test the complete API flow:

def test_user_crud_flow():
    client = TestClient(app)

    # Create user
    create_response = client.post("/api/v1/users", json=test_user_data)
    assert create_response.status_code == 201
    user = create_response.json()
    user_id = user["id"]

    # Get user
    get_response = client.get(f"/api/v1/users/{user_id}")
    assert get_response.status_code == 200
    assert get_response.json()["id"] == user_id

    # Update user
    update_data = {"first_name": "Jane"}
    update_response = client.put(f"/api/v1/users/{user_id}", json=update_data)
    assert update_response.status_code == 200
    assert update_response.json()["first_name"] == "Jane"

    # Delete user
    delete_response = client.delete(f"/api/v1/users/{user_id}")
    assert delete_response.status_code == 204

    # Verify deletion
    get_deleted_response = client.get(f"/api/v1/users/{user_id}")
    assert get_deleted_response.status_code == 404

๐Ÿš€ Best Practicesยถ

1. Keep Controllers Thinยถ

Controllers should delegate to the application layer:

# Good - Thin controller
class UsersController(ControllerBase):
    @post("/", response_model=UserDto)
    async def create_user(self, create_user_dto: CreateUserDto) -> UserDto:
        command = self.mapper.map(create_user_dto, CreateUserCommand)
        result = await self.mediator.execute_async(command)
        return self.process(result)

# Avoid - Business logic in controller
class UsersController(ControllerBase):
    @post("/", response_model=UserDto)
    async def create_user(self, create_user_dto: CreateUserDto) -> UserDto:
        # Validate email
        if not self.is_valid_email(create_user_dto.email):
            raise HTTPException(400, "Invalid email")

        # Check if user exists
        existing = await self.user_repo.get_by_email(create_user_dto.email)
        if existing:
            raise HTTPException(409, "User exists")

        # Create user
        user = User(...)
        # ... more business logic

2. Use DTOs for API Contractsยถ

Always use DTOs to define your API contracts:

# API DTOs
class CreateUserDto(BaseModel):
    email: str
    first_name: str
    last_name: str

class UserDto(BaseModel):
    id: str
    email: str
    first_name: str
    last_name: str
    created_at: datetime

# Domain entities stay separate
class User(Entity[str]):
    def __init__(self, email: str, first_name: str, last_name: str):
        # Domain logic
        pass

3. Consistent Error Handlingยถ

Use consistent patterns for error handling:

class UsersController(ControllerBase):

    @get("/{user_id}", 
         response_model=UserDto,
         responses={
             404: {"description": "User not found"},
             400: {"description": "Invalid user ID format"}
         })
    async def get_user(self, user_id: str) -> UserDto:
        # Validate input format
        if not self.is_valid_uuid(user_id):
            return self.bad_request("Invalid user ID format")

        # Execute query
        query = GetUserByIdQuery(user_id=user_id)
        result = await self.mediator.execute_async(query)

        # Process will handle 404 automatically
        return self.process(result)

4. Document Your APIsยถ

Provide comprehensive API documentation:

class UsersController(ControllerBase):

    @post("/",
          response_model=UserDto,
          status_code=status.HTTP_201_CREATED,
          summary="Create a new user",
          description="Creates a new user account in the system",
          response_description="The created user",
          tags=["User Management"])
    async def create_user(self, create_user_dto: CreateUserDto) -> UserDto:
        """
        Create a new user account.

        - **email**: User's email address (must be unique)
        - **first_name**: User's first name
        - **last_name**: User's last name

        Returns the created user with generated ID and timestamps.
        """
        command = self.mapper.map(create_user_dto, CreateUserCommand)
        result = await self.mediator.execute_async(command)
        return self.process(result)

5. Version Your APIsยถ

Plan for API versioning:

# v1 controller
class V1UsersController(ControllerBase):
    def __init__(self, service_provider, mapper, mediator):
        super().__init__(service_provider, mapper, mediator)
```python
from neuroglia.hosting.web import WebApplicationBuilder
from neuroglia.mvc import ControllerDiscovery

def create_pizzeria_app():
    """Configure Mario's Pizzeria application with controllers"""
    builder = WebApplicationBuilder()

    # Configure services
    builder.services.add_mediator()
    builder.services.add_auto_mapper()

    # Add controllers with automatic discovery
    builder.services.add_controllers([
        "api.controllers.orders_controller",
        "api.controllers.menu_controller", 
        "api.controllers.kitchen_controller",
        "api.controllers.reports_controller",
        "api.controllers.auth_controller"
    ])

    # Build application
    app = builder.build()

    # Configure controller routes with prefixes
    app.include_router(OrdersController().router, prefix="/api/orders", tags=["Orders"])
    app.include_router(MenuController().router, prefix="/api/menu", tags=["Menu"])
    app.include_router(KitchenController().router, prefix="/api/kitchen", tags=["Kitchen"])
    app.include_router(ReportsController().router, prefix="/api/reports", tags=["Reports"])
    app.include_router(AuthController().router, prefix="/api/auth", tags=["Authentication"])

    # Add exception handlers
    app.add_exception_handler(PizzeriaException, pizzeria_exception_handler)
    app.add_exception_handler(RequestValidationError, validation_exception_handler)

    return app

# Environment-specific controller registration
def configure_development_controllers(builder: WebApplicationBuilder):
    """Add development-specific controllers"""
    # Add mock data controller for testing
    builder.services.add_controller(MockDataController)

def configure_production_controllers(builder: WebApplicationBuilder):
    """Add production-specific controllers"""
    # Add monitoring and health check controllers
    builder.services.add_controller(HealthController)
    builder.services.add_controller(MetricsController)

Controller Middleware and Interceptorsยถ

Add cross-cutting concerns to controllers:

from fastapi import Request, Response
from fastapi.middleware.base import BaseHTTPMiddleware
import time
import logging

class PizzeriaRequestLoggingMiddleware(BaseHTTPMiddleware):
    """Log all pizzeria API requests"""

    async def dispatch(self, request: Request, call_next):
        start_time = time.time()

        # Log incoming request
        logging.info(f"Incoming {request.method} {request.url}")

        # Process request
        response = await call_next(request)

        # Log response
        process_time = time.time() - start_time
        logging.info(f"Completed {request.method} {request.url} - "
                    f"Status: {response.status_code} - "
                    f"Duration: {process_time:.2f}s")

        return response

class OrderValidationMiddleware(BaseHTTPMiddleware):
    """Validate order-related requests"""

    async def dispatch(self, request: Request, call_next):
        if request.url.path.startswith("/api/orders"):
            # Add order-specific validation
            if request.method == "POST":
                # Validate business hours
                if not self.is_business_hours():
                    return JSONResponse(
                        status_code=400,
                        content={"error": "Pizzeria is currently closed"}
                    )

        return await call_next(request)

    def is_business_hours(self) -> bool:
        """Check if pizzeria is open for orders"""
        from datetime import datetime
        now = datetime.now()
        return 11 <= now.hour <= 22  # Open 11 AM to 10 PM

# Add middleware to application
app.add_middleware(PizzeriaRequestLoggingMiddleware)
app.add_middleware(OrderValidationMiddleware)

๐Ÿงช Controller Testing Patternsยถ

Unit Testing Controllersยถ

Test controllers with mocked dependencies:

import pytest
from unittest.mock import AsyncMock, Mock
from fastapi.testclient import TestClient
from neuroglia.mediation import OperationResult

class TestOrdersController:
    """Unit tests for orders controller"""

    @pytest.fixture
    def mock_mediator(self):
        """Mock mediator for testing"""
        mediator = AsyncMock()
        return mediator

    @pytest.fixture
    def orders_controller(self, mock_mediator):
        """Orders controller with mocked dependencies"""
        service_provider = Mock()
        mapper = Mock()

        controller = OrdersController(service_provider, mapper, mock_mediator)
        return controller

    @pytest.mark.asyncio
    async def test_place_order_success(self, orders_controller, mock_mediator):
        """Test successful order placement"""
        # Arrange
        order_request = PlaceOrderDto(
            customer_name="John Doe",
            customer_phone="+1234567890",
            customer_address="123 Main St",
            pizzas=[PizzaOrderDto(name="Margherita", size="large", quantity=1)],
            payment_method="card"
        )

        expected_order = OrderDto(
            id="order_123",
            customer_name="John Doe",
            status="received",
            total_amount=15.99
        )

        mock_mediator.execute_async.return_value = OperationResult.success(expected_order)

        # Act
        result = await orders_controller.place_order(order_request)

        # Assert
        assert result.id == "order_123"
        assert result.customer_name == "John Doe"
        mock_mediator.execute_async.assert_called_once()

    @pytest.mark.asyncio
    async def test_place_order_validation_error(self, orders_controller, mock_mediator):
        """Test order placement with validation error"""
        # Arrange
        invalid_order = PlaceOrderDto(
            customer_name="",  # Invalid empty name
            customer_phone="invalid",  # Invalid phone
            customer_address="",  # Invalid empty address
            pizzas=[],  # No pizzas
            payment_method="invalid"  # Invalid payment method
        )

        # Act & Assert
        with pytest.raises(ValidationError):
            await orders_controller.place_order(invalid_order)

@pytest.mark.integration
class TestOrdersControllerIntegration:
    """Integration tests for orders controller"""

    @pytest.fixture
    def test_client(self):
        """Test client for integration testing"""
        app = create_pizzeria_app()
        return TestClient(app)

    def test_get_menu_integration(self, test_client):
        """Test menu retrieval integration"""
        response = test_client.get("/api/menu/pizzas")

        assert response.status_code == 200
        menu = response.json()
        assert isinstance(menu, list)

        # Validate pizza structure
        if menu:
            pizza = menu[0]
            assert "id" in pizza
            assert "name" in pizza
            assert "base_price" in pizza

    def test_place_order_integration(self, test_client):
        """Test order placement integration"""
        order_data = {
            "customer_name": "Integration Test Customer",
            "customer_phone": "+1234567890",
            "customer_address": "123 Test Street, Test City",
            "pizzas": [
                {
                    "name": "Margherita",
                    "size": "large",
                    "toppings": ["extra_cheese"],
                    "quantity": 1
                }
            ],
            "payment_method": "card"
        }

        response = test_client.post("/api/orders/", json=order_data)

        assert response.status_code == 201
        order = response.json()
        assert order["customer_name"] == "Integration Test Customer"
        assert order["status"] == "received"
        assert "id" in order

๏ฟฝ API Documentation Generationยถ

OpenAPI Configurationยถ

Configure comprehensive API documentation:

from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi

def create_pizzeria_app_with_docs():
    """Create Mario's Pizzeria app with enhanced documentation"""
    app = create_pizzeria_app()

    # Custom OpenAPI schema
    def custom_openapi():
        if app.openapi_schema:
            return app.openapi_schema

        openapi_schema = get_openapi(
            title="Mario's Pizzeria API",
            version="1.0.0",
            description="""
            # ๐Ÿ• Mario's Pizzeria API

            Welcome to Mario's Pizzeria API! This API provides comprehensive 
            functionality for managing pizza orders, menu items, kitchen workflow, 
            and customer interactions.

            ## Features

            - **Order Management**: Place, track, and manage pizza orders
            - **Menu Administration**: Manage pizzas, toppings, and availability
            - **Kitchen Workflow**: Handle order preparation and status updates
            - **Customer Authentication**: Secure customer account management
            - **Staff Portal**: Role-based access for staff operations
            - **Analytics**: Revenue and performance reporting

            ## Authentication

            The API uses OAuth 2.0 with JWT tokens:

            - **Customers**: Phone-based OTP authentication
            - **Staff**: Username/password with role-based permissions

            ## Rate Limiting

            - **Customers**: 100 requests per hour
            - **Staff**: 500 requests per hour
            - **Managers**: Unlimited
            """,
            routes=app.routes,
        )

        # Add custom tags for better organization
        openapi_schema["tags"] = [
            {
                "name": "Orders",
                "description": "Customer order management and tracking"
            },
            {
                "name": "Menu",
                "description": "Pizza menu and item management"
            },
            {
                "name": "Kitchen",
                "description": "Kitchen operations and workflow"
            },
            {
                "name": "Authentication",
                "description": "Customer and staff authentication"
            },
            {
                "name": "Reports",
                "description": "Analytics and reporting (Manager only)"
            }
        ]

        # Add security schemes
        openapi_schema["components"]["securitySchemes"] = {
            "BearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "JWT"
            },
            "CustomerAuth": {
                "type": "oauth2",
                "flows": {
                    "password": {
                        "tokenUrl": "/api/auth/customer/login",
                        "scopes": {
                            "customer": "Customer order access"
                        }
                    }
                }
            },
            "StaffAuth": {
                "type": "oauth2",
                "flows": {
                    "password": {
                        "tokenUrl": "/api/auth/staff/login",
                        "scopes": {
                            "kitchen": "Kitchen operations",
                            "manager": "Management functions"
                        }
                    }
                }
            }
        }

        app.openapi_schema = openapi_schema
        return app.openapi_schema

    app.openapi = custom_openapi
    return app

This documentation demonstrates MVC controller patterns using Mario's Pizzeria as a consistent example throughout the Neuroglia framework. The examples show real-world API design with authentication, validation, error handling, and comprehensive testing.