Best Practices
This guide covers production-tested patterns and anti-patterns for Genro Routes.
Router Design
Keep Routers Focused
Each router should have a clear, single responsibility:
# Good: Focused routers
class UserService(RoutingClass):
def __init__(self):
self.api = Router(self, name="api")
@route("api")
def list_users(self): ...
@route("api")
def get_user(self, user_id: int): ...
@route("api")
def create_user(self, data: dict): ...
# Bad: Router doing too much
class EverythingService(RoutingClass):
def __init__(self):
self.api = Router(self, name="api")
@route("api")
def list_users(self): ...
@route("api")
def send_email(self): ...
@route("api")
def generate_report(self): ...
Use Meaningful Names
Router and handler names should be self-documenting:
# Good: Clear, descriptive names
class OrderService(RoutingClass):
def __init__(self):
self.orders = Router(self, name="orders")
@route("orders")
def list_pending(self): ...
@route("orders")
def mark_shipped(self, order_id: int): ...
# Bad: Vague names
class Service(RoutingClass):
def __init__(self):
self.api = Router(self, name="api")
@route("api")
def do_stuff(self): ...
@route("api")
def process(self, id): ...
Leverage Prefixes for Organization
Use prefixes to group related handlers while keeping public names clean:
class AdminAPI(RoutingClass):
def __init__(self):
self.admin = Router(self, name="admin", prefix="admin_")
@route("admin")
def admin_list_users(self):
"""Exposed as 'list_users'"""
...
@route("admin")
def admin_delete_user(self, user_id: int):
"""Exposed as 'delete_user'"""
...
Hierarchy Design
Flat is Better Than Deep
Prefer shallow hierarchies over deeply nested ones:
# Good: Shallow, navigable hierarchy
app.api.node("users/list")()
app.api.node("orders/create")()
app.api.node("reports/sales")()
# Bad: Too deep, hard to navigate
app.api.node("v1/internal/services/users/management/list")()
Use Branch Routers for Organization
Branch routers provide namespace organization without handlers:
class Application(RoutingClass):
def __init__(self):
# Branch router as namespace container
self.api = Router(self, name="api", branch=True)
# Attach actual services
self.attach_instance(self.users, name="users")
self.attach_instance(self.orders, name="orders")
Attaching Child Instances
Child instances can be attached directly — storing as an attribute is optional:
class Parent(RoutingClass):
def __init__(self):
self.api = Router(self, name="api")
# Both approaches work — the router tree keeps a strong reference
self.attach_instance(ChildService(), name="child")
# Retrieve the child instance later if needed
child = parent.routing.instance("api/child")
Plugin Usage
Apply Plugins at the Right Level
Attach plugins where they make sense:
# Good: Logging at root, validation where needed
class Application(RoutingClass):
def __init__(self):
self.api = Router(self, name="api").plug("logging") # All handlers logged
self.public = PublicAPI() # No validation needed
self.admin = AdminAPI() # Has its own pydantic plugin
self.attach_instance(self.public, name="public")
self.attach_instance(self.admin, name="admin")
class AdminAPI(RoutingClass):
def __init__(self):
self.api = Router(self, name="api").plug("pydantic") # Strict validation
Compose Simple Plugins
Multiple focused plugins beat one complex plugin:
# Good: Composable plugins
self.api = Router(self, name="api")\
.plug("logging")\
.plug("pydantic")\
.plug("caching")
# Bad: Monolithic plugin
self.api = Router(self, name="api").plug("do_everything")
Configure Plugins Explicitly
Don’t rely on defaults for production:
# Good: Explicit configuration
svc.routing.configure("api:logging/_all_", level="info", enabled=True)
svc.routing.configure("api:pydantic/_all_", strict=True)
# Bad: Implicit defaults everywhere
svc.api.plug("logging").plug("pydantic") # What's the config?
Error Handling
Let Errors Propagate
Don’t swallow exceptions in handlers:
# Good: Let errors propagate
@route("api")
def create_user(self, data: dict):
user = self.repository.create(data) # May raise
return user
# Bad: Swallowing errors
@route("api")
def create_user(self, data: dict):
try:
user = self.repository.create(data)
return user
except Exception:
return None # Caller has no idea what happened
Use Plugin Wrappers for Cross-Cutting Concerns
Handle errors consistently via plugins:
class ErrorHandlerPlugin(BasePlugin):
plugin_code = "error_handler"
plugin_description = "Consistent error handling"
def wrap_handler(self, router, entry, call_next):
def wrapper(*args, **kwargs):
try:
return call_next(*args, **kwargs)
except ValidationError as e:
return {"error": "validation", "details": str(e)}
except NotFoundError as e:
return {"error": "not_found", "details": str(e)}
return wrapper
Testing
Test Handlers in Isolation
Test handler logic independently of routing:
def test_user_service_list():
svc = UserService()
# Test via router
result = svc.api.node("list_users")()
assert isinstance(result, list)
def test_user_service_create():
svc = UserService()
result = svc.api.node("create_user")({"name": "Alice"})
assert result["name"] == "Alice"
Test Hierarchies
Verify hierarchy structure and access:
def test_application_hierarchy():
app = Application()
# Verify structure
nodes = app.api.nodes()
assert "users" in nodes["routers"]
assert "orders" in nodes["routers"]
# Verify access
users = app.api.node("users/list_users")()
assert isinstance(users, list)
Test Plugin Behavior
Test that plugins affect handler execution:
def test_logging_plugin_called(caplog):
svc = LoggedService()
svc.api.node("action")()
assert "action" in caplog.text
Performance
Attach Plugins Early
Attach plugins during router creation for optimal handler wrapping:
# Good: Plugins attached during construction
class Service(RoutingClass):
def __init__(self):
self.api = Router(self, name="api")\
.plug("logging")\
.plug("pydantic")
@route("api")
def action(self):
return "done"
Cache Handler References
If calling the same handler repeatedly, cache the reference:
# Good: Cache for repeated calls
node = svc.api.node("process")
for item in items:
node(item)
# Less efficient: Lookup every time
for item in items:
svc.api.node("process")(item)
Anti-Patterns
Global State in Plugins
Plugins should not share global state:
# Bad: Global state
_global_cache = {}
class CachePlugin(BasePlugin):
def wrap_handler(self, router, entry, call_next):
def wrapper(*args):
key = (entry.name, args)
if key in _global_cache: # Shared across all instances!
return _global_cache[key]
...
# Good: Per-instance state
class CachePlugin(BasePlugin):
__slots__ = ("_cache",)
def __init__(self, router, **config):
self._cache = {} # Per-instance
super().__init__(router, **config)
Circular Dependencies
Avoid circular attachments:
# Bad: Circular reference
class A(RoutingClass):
def __init__(self, b):
self.api = Router(self, name="api")
self.b = b
self.attach_instance(b, name="b")
class B(RoutingClass):
def __init__(self, a):
self.api = Router(self, name="api")
self.a = a
self.attach_instance(a, name="a") # Circular!
Over-Configuration
Don’t configure what doesn’t need configuration:
# Bad: Over-engineered
svc.routing.configure("api:logging/handler1", level="info")
svc.routing.configure("api:logging/handler2", level="info")
svc.routing.configure("api:logging/handler3", level="info")
# ... 50 more lines
# Good: Use defaults and override exceptions
svc.routing.configure("api:logging/_all_", level="info")
svc.routing.configure("api:logging/debug_*", level="debug")
Summary
Do |
Don’t |
|---|---|
Keep routers focused |
Mix unrelated handlers |
Use meaningful names |
Use vague names |
Use |
Rely on global variables for child access |
Apply plugins at right level |
Over-apply plugins |
Let errors propagate |
Swallow exceptions |
Test handlers in isolation |
Only test via HTTP |
Cache handler references |
Lookup repeatedly |
Use per-instance state |
Use global state |
Next Steps
API Reference - Complete API documentation
Plugin Development - Create custom plugins
Hierarchies - Advanced routing patterns