Skip to main content

Flavors

Flavors are the fundamental building blocks of Tasteful applications. They represent modular, self-contained components that can be composed together to create complete applications. Each flavor encapsulates a specific domain or feature area with its own HTTP interface, business logic, and data access patterns.

What is a Flavor?

A Flavor is a collection of related functionality organized into distinct layers: Think of flavors as “micro-applications” that can run independently or be combined with other flavors to build complete systems. Each flavor follows the same architectural patterns, making them predictable and maintainable.

Basic Flavor Structure

A complete flavor consists of four main components working together:
from tasteful.base_flavor import BaseFlavor, BaseController, BaseService, BaseConfig
from tasteful.decorators.controller import Get, Post
from tasteful.repositories import SQLModelRepository

class UserConfig(BaseConfig):
    """Configuration for User flavor."""
    database_url: str = "sqlite:///users.db"
    max_users: int = 1000

class UserRepository(SQLModelRepository):
    """Repository for user data access."""
    
    def __init__(self, database_url: str):
        super().__init__(database_url=database_url)
    
    def get_by_id(self, user_id: int):
        with self.get_session() as session:
            return session.get(User, user_id)
    
    def create(self, user_data: dict):
        with self.get_session() as session:
            user = User(**user_data)
            session.add(user)
            session.commit()
            session.refresh(user)
            return user

class UserService(BaseService):
    """Service for user business logic."""
    
    def __init__(self, user_repository: UserRepository):
        super().__init__()
        self.user_repository = user_repository
    
    def get_user(self, user_id: int):
        # Business logic: validate input
        if user_id <= 0:
            raise ValueError("User ID must be positive")
        
        # Delegate to repository
        user = self.user_repository.get_by_id(user_id)
        if not user:
            raise UserNotFoundError("User not found")
        
        return user
    
    def create_user(self, user_data: dict):
        # Business logic: validation and processing
        if not user_data.get("email"):
            raise ValueError("Email is required")
        
        # Delegate to repository
        return self.user_repository.create(user_data)

class UserController(BaseController):
    """Controller for user HTTP endpoints."""
    
    def __init__(self, user_service: UserService):
        super().__init__(prefix="/users", tags=["users"])
        self.user_service = user_service
    
    @Get("/{user_id}")
    async def get_user(self, user_id: int):
        # Controller delegates to service
        return self.user_service.get_user(user_id)
    
    @Post("/")
    async def create_user(self, user_data: dict):
        # Controller delegates to service
        return self.user_service.create_user(user_data)

class UserFlavor(BaseFlavor):
    """User management flavor."""
    
    def __init__(self):
        super().__init__(
            controller=UserController,
            services=[UserService],
            repositories=[UserRepository],
            config=UserConfig
        )
This structure demonstrates the clear separation of concerns:
  • Controller handles HTTP requests and delegates to services
  • Service contains all business logic and coordinates with repositories
  • Repository manages data access and persistence
  • Configuration provides settings and dependencies

Key Features

1. Constructor-Based Initialization

Flavors use constructor-based initialization where you specify the components that make up your flavor:
class UserFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=UserController,
            services=[UserService, EmailService],
            repositories=[UserRepository],
            config=UserConfig
        )

class UserController(BaseController):
    def __init__(self, user_service: UserService, email_service: EmailService):
        super().__init__(prefix="/users", tags=["users"])
        self.user_service = user_service
        self.email_service = email_service
    
    @Get("/{user_id}")
    async def get_user(self, user_id: int):
        # Controller stays thin - delegates to service
        return self.user_service.get_user(user_id)
    
    @Post("/{user_id}/welcome")
    async def send_welcome(self, user_id: int):
        # Service orchestration through controller
        user = self.user_service.get_user(user_id)
        return self.email_service.send_welcome_email(user)
The dependency injection system automatically wires up the dependencies between components based on their constructor parameters.

2. HTTP Route Decorators

Controllers use decorators to define HTTP endpoints. These decorators mirror FastAPI’s functionality while integrating with Tasteful’s architecture:
from tasteful.decorators.controller import Get, Post, Put, Delete, Patch
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
    
    @Get("/")
    async def list_users(self, skip: int = 0, limit: int = 100):
        return self.user_service.list_users(skip=skip, limit=limit)
    
    @Post("/")
    async def create_user(self, user_data: dict):
        return self.user_service.create_user(user_data)
    
    @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)
For detailed information about route decorators and controller patterns, see the Controller concept page.

3. URL Prefixing and Organization

All routes in a controller are automatically prefixed based on the controller configuration, providing clean URL organization:
class UserController(BaseController):
    def __init__(self, user_service: UserService):
        super().__init__(prefix="/api/v1/users", tags=["users"])
        self.user_service = user_service
    
    @Get("/")          # Becomes: GET /api/v1/users/
    async def list_users(self): 
        return self.user_service.list_users()
    
    @Get("/{user_id}") # Becomes: GET /api/v1/users/{user_id}
    async def get_user(self, user_id: int): 
        return self.user_service.get_user(user_id)
    
    @Post("/")         # Becomes: POST /api/v1/users/
    async def create_user(self, user_data: dict):
        return self.user_service.create_user(user_data)
This automatic prefixing ensures consistent URL patterns across your application and makes it easy to organize related endpoints together.

4. Dependency Injection

Flavors leverage Tasteful’s dependency injection system to automatically wire up components. Dependencies are resolved based on constructor parameters:
class OrderService(BaseService):
    def __init__(
        self,
        order_repository: OrderRepository,    # ← Injected automatically
        user_service: UserService,           # ← Injected automatically
        payment_service: PaymentService      # ← Injected automatically
    ):
        super().__init__()
        self.order_repository = order_repository
        self.user_service = user_service
        self.payment_service = payment_service

class OrderFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=OrderController,
            services=[OrderService, UserService, PaymentService],  # ← Registered for injection
            repositories=[OrderRepository, UserRepository],
            config=OrderConfig
        )
The dependency injection system handles the complexity of wiring up your components, making your code cleaner and more testable.

Built-in Flavors

Tasteful comes with several pre-built flavors for common functionality: These built-in flavors follow the same architectural patterns as custom flavors and can be used as examples for building your own flavors.

Flavor Composition

Combine multiple flavors to build complete applications:
from tasteful import TastefulApp
from tasteful.flavors import HealthFlavor
from my_app.flavors import UserFlavor, OrderFlavor

app = TastefulApp(
    title="My E-commerce API",
    version="1.0.0",
    flavors=[
        HealthFlavor,
        UserFlavor, 
        OrderFlavor
    ],
    authentication_backends=[]
)

Best Practices

1. Single Responsibility Principle

Each flavor should have a single, well-defined responsibility that aligns with a specific domain or feature area:
# ✅ Good: Focused on user management
class UserFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=UserController,  # Only user-related endpoints
            services=[UserService, UserValidationService],
            repositories=[UserRepository],
            config=UserConfig
        )

# ❌ Bad: Mixed responsibilities  
class UserOrderFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=UserOrderController,  # Mixed user and order endpoints
            services=[UserService, OrderService],  # Mixed concerns
            config=MixedConfig
        )

2. Meaningful Naming

Use descriptive names that clearly communicate the purpose of flavors and their components:
class PaymentProcessingFlavor(BaseFlavor):
    def __init__(self):
        super().__init__(
            controller=PaymentController,  # Clear controller name
            services=[PaymentService, BillingService],
            repositories=[PaymentRepository, TransactionRepository],
            config=PaymentConfig
        )

class PaymentController(BaseController):
    def __init__(self, payment_service: PaymentService):
        super().__init__(
            prefix="/payments",  # RESTful prefix
            tags=["payments", "billing"]  # Descriptive tags
        )
        self.payment_service = payment_service

Migration from Microservices

When migrating from microservices to flavors:
  1. Map service boundaries - Each microservice typically becomes a flavor
  2. Consolidate shared logic - Move common code to shared services or base classes
  3. Simplify data access - Replace multiple databases with shared repositories where appropriate
  4. Replace HTTP calls - Convert inter-service HTTP calls to direct service method calls
  5. Maintain transaction boundaries - Ensure data consistency is preserved during migration
# Before: Microservice HTTP call
response = requests.post("http://user-service/api/users", json=user_data)
user = response.json()

# After: Direct service call in flavor
user = self.user_service.create_user(user_data)
When migrating, carefully consider data consistency and transaction boundaries that may have been handled at the microservice level. You may need to implement distributed transaction patterns or redesign your data model.
I