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:
- Map service boundaries - Each microservice typically becomes a flavor
- Consolidate shared logic - Move common code to shared services or base classes
- Simplify data access - Replace multiple databases with shared repositories where appropriate
- Replace HTTP calls - Convert inter-service HTTP calls to direct service method calls
- 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.