Genro Routes - Frequently Asked Questions
What is Genro Routes?
What problem does Genro Routes solve?
Question: I have many methods in a class and want to call them dynamically by string name. How can I organize them better?
Answer: Genro Routes lets you create a “router” that maps string names to Python methods, with per-instance isolation and hierarchy support. Instead of manually managing a dictionary of handlers, you use the @route() decorator and Genro Routes handles the rest.
Example:
from genro_routes import RoutingClass, Router, route
class OrdersAPI(RoutingClass):
def __init__(self):
self.api = Router(self, name="orders")
@route("orders")
def list(self):
return ["order-1", "order-2"]
@route("orders")
def create(self, payload: dict):
return {"status": "created", **payload}
orders = OrdersAPI()
orders.api.node("list")() # Calls list()
orders.api.node("create")({"name": "order-3"}) # Calls create()
Genro Routes vs function dictionary?
Question: Why not just use a dictionary {"list": self.list, "create": self.create}?
Answer: Genro Routes offers:
Plugin system: add logging, validation, audit without touching handlers
Hierarchies: organize routers in trees with
attach_instance()(method onRoutingClass)Metadata: each handler can have tags, channels, configurations
Introspection:
router.nodes()to explore structureIsolation: each instance has its own router with independent plugins
For simple apps, a dictionary may suffice. For complex services, Genro Routes provides structure and extensibility.
Is Genro Routes a web framework?
Question: Does Genro Routes replace FastAPI/Flask?
Answer: No. Genro Routes is an internal routing engine for organizing Python methods. It doesn’t handle HTTP, WebSocket, or networking. It’s used inside an application for:
CLI tools
Internal orchestrators
Service composition
Dynamic dashboards
You can use Genro Routes alongside FastAPI to organize your internal handlers before exposing them via HTTP.
Core Concepts
What is a Router?
Question: What exactly does a Router do?
Answer: A Router is an object that:
Registers handlers: methods decorated with
@route()Resolves by name:
router.node("method_name")→ callable RouterNodeApplies plugins: intercepts decoration and execution
Is isolated per instance: each object has its own router
class Service(RoutingClass):
def __init__(self, label: str):
self.label = label
self.api = Router(self, name="api")
@route("api")
def info(self):
return f"service:{self.label}"
s1 = Service("alpha")
s2 = Service("beta")
s1.api.node("info")() # "service:alpha"
s2.api.node("info")() # "service:beta"
Each instance (s1, s2) has a separate and isolated router.
How does the @route decorator work?
Question: What does @route("api") exactly do?
Answer: The @route("router_name") decorator marks a method to be registered in a specific router. When you create the instance and call Router(self, name="api"), the router finds all methods marked with @route("api") and registers them automatically.
Options:
@route("api") # Auto name (method name)
def list_users(self): ...
@route("api", name="users") # Explicit name
def handle_users(self): ...
# With Router(prefix="handle_")
@route("api")
def handle_create(self): ... # Registered as "create" (strips prefix)
What is RoutingClass?
Question: Do I always need to inherit from RoutingClass?
Answer: Recommended but not required. RoutingClass provides:
obj.routingproxy to access all routersobj.routing.configure()for global configurationAutomatic router registry management
Without RoutingClass you can still use Router directly, but you lose the unified proxy.
Hierarchies and Child Routers
How do I organize nested routers?
Question: I have an application with modules (sales, finance, admin) that I want to organize hierarchically. How?
Answer: Use attach_instance() (a method on RoutingClass) to connect child instances:
class Dashboard(RoutingClass):
def __init__(self):
self.api = Router(self, name="api")
self.sales = SalesModule()
self.finance = FinanceModule()
# Attach child instances (1:1 shortcut — each child has a single router)
self.attach_instance(self.sales, name="sales")
self.attach_instance(self.finance, name="finance")
dashboard = Dashboard()
# Access with path separator
dashboard.api.node("sales/report")()
dashboard.api.node("finance/summary")()
How do I access child routers?
Question: Once connected, how do I call child handlers?
Answer: Use path separator /:
# Path separator
dashboard.api.node("sales/report")()
# Or direct access
dashboard.sales.api.node("report")()
# Introspection
nodes = dashboard.api.nodes()
# {
# "entries": {...},
# "routers": {
# "sales": {...},
# "finance": {...}
# }
# }
Do plugins inherit to children?
Question: If I attach a plugin to the parent router, do children see it?
Answer: Yes, automatically. Plugins propagate from parent to children:
class Parent(RoutingClass):
def __init__(self):
self.api = Router(self, name="api").plug("logging", level="debug")
self.child_obj = Child()
self.attach_instance(self.child_obj, name="child")
# Child automatically inherits logging plugin
parent = Parent()
parent.api.node("child/method")() # Logs with level=debug
Plugin System
What are plugins?
Question: What is a plugin in Genro Routes and what is it for?
Answer: A plugin extends router behavior without modifying handlers. Plugins intercept:
Decoration (
on_decore): when a handler is registeredExecution (
wrap_handler): when a handler is called
Use cases:
Logging: record all calls
Validation: check input with Pydantic
Audit: track who/when/what
How do I use built-in plugins?
Question: Does Genro Routes have ready-to-use plugins?
Answer: Yes, 5 built-in plugins:
1. LoggingPlugin - Automatic logging
router = Router(self, name="api").plug("logging")
router.node("method")() # Auto-logs the call
2. PydanticPlugin - Input validation + response schemas
@route("api")
def concat(self, text: str, number: int = 1) -> str:
return f"{text}:{number}"
router.plug("pydantic")
router.node("concat")("hello", 3) # OK → "hello:3"
router.node("concat")(123, "oops") # ValidationError
# Response schema auto-generated from return type annotation
router._entries["concat"].metadata["pydantic"]["response_schema"]
# {"type": "string"}
3. AuthPlugin - Role-based access control
@route("api", auth_rule="admin")
def admin_action(self):
return "secret"
router.plug("auth")
router.node("admin_action", auth_tags="admin")() # OK
router.node("admin_action", auth_tags="guest")() # NotAuthorized
4. EnvPlugin - Capability-based filtering
@route("api", env_requires="redis")
def cached_action(self):
return "cached"
router.plug("env")
# Entry only visible if instance has "redis" capability
5. OpenAPIPlugin - Schema metadata and response schemas
@route("api", openapi_method="post", openapi_tags="users")
def create_user(self, name: str) -> dict:
return {"name": name}
router.plug("openapi")
# Provides metadata for OpenAPI schema generation
# Response schemas auto-included from return type annotations
How do I configure plugins at runtime?
Question: I want to change plugin configuration after creating the router.
Answer: Use routing.configure():
# Global for all handlers
obj.routing.configure("api:logging/_all_", level="warning")
# For specific handler
obj.routing.configure("api:logging/create", enabled=False)
# With glob patterns
obj.routing.configure("api:logging/admin_*", level="debug")
# Query configuration
report = obj.routing.configure("?")
Can I create custom plugins?
Question: How do I write a custom plugin?
Answer: Inherit from BasePlugin and implement the hooks:
from genro_routes.plugins import BasePlugin
class AuditPlugin(BasePlugin):
def on_decore(self, router, func, entry):
"""Called when handler is registered"""
entry.metadata["audited"] = True
def wrap_handler(self, router, entry, call_next):
"""Called when handler is executed"""
def wrapper(*args, **kwargs):
print(f"[AUDIT] Calling {entry.name}")
result = call_next(*args, **kwargs)
print(f"[AUDIT] Result: {result}")
return result
return wrapper
# Register and use
Router.register_plugin(AuditPlugin)
router = Router(self, name="api").plug("audit")
Advanced Use Cases
How do I handle errors and defaults?
Question: What happens if I call a non-existent handler?
Answer: Check node.error to see if the node resolved correctly:
# node() returns a RouterNode - check error status
node = router.node("missing")
if node.error:
print(f"Handler error: {node.error}")
# RouterNode is always callable - errors raise on invocation
node = router.node("my_handler")
result = node() # Invoke the handler (raises if error)
# Calling a node with error raises the appropriate exception
from genro_routes import NotFound
try:
router.node("missing")()
except NotFound:
print("Handler not found")
What exceptions can node() raise?
Question: What exceptions should I catch when calling a RouterNode?
Answer: Three main exceptions:
NotFound: Path not found, no default_entry, or partial args don’t match signatureNotAuthorized: Auth tags provided but don’t match (403)NotAuthenticated: Auth required but not provided (401)
from genro_routes import NotFound, NotAuthorized, NotAuthenticated
try:
router.node("handler")()
except NotAuthenticated:
# 401 - no auth provided
pass
except NotAuthorized:
# 403 - auth provided but wrong
pass
except NotFound:
# 404 - path not found
pass
You can also map these to custom exceptions:
node = router.node("handler", errors={
'not_found': HTTPNotFound,
'not_authorized': HTTPForbidden,
'not_authenticated': HTTPUnauthorized,
})
What is a root node?
Question: What do I get when calling node("/")?
Answer: A root node pointing to the router’s default entry:
node = router.node("/") # or node("")
# Check node state
node.path # ""
node.error # None if default_entry exists
# If default_entry exists
node() # calls default_entry
# If no default_entry
node() # raises NotFound
How do I introspect the structure?
Question: I want to see all registered entries and child routers.
Answer: Use nodes():
# Structure snapshot
nodes = router.nodes()
# {
# "name": "api",
# "router": <Router>,
# "instance": <owner>,
# "plugin_info": {...},
# "entries": {
# "list": {"callable": <function>, "doc": "...", ...},
# "create": {...}
# },
# "routers": {
# "sales": {...}
# }
# }
# With filters (using AuthPlugin)
admin_only = router.nodes(auth_tags="admin")
# Generate OpenAPI schema
schema = router.nodes(mode="openapi")
nodes() parameters:
basepath: Start from a specific point in the hierarchylazy: Return router references instead of expandingmode: Output format ("openapi"for OpenAPI schema)
Comparisons
Genro Routes vs decorator dispatch?
Question: Why not use functools.singledispatch?
Answer:
singledispatch→ dispatch by type of first argumentGenro Routes → dispatch by string name with metadata, plugins, hierarchies
Different use cases: singledispatch for typed polymorphism, Genro Routes for dynamic routing.
Troubleshooting
“No plugin named ‘X’ attached to router”
Problem: AttributeError: No plugin named 'logging' attached to router
Solution: The plugin wasn’t attached. Use .plug():
router.plug("logging") # Now router.logging exists
“Handler name collision”
Problem: Two methods with the same name registered on the same router.
Solution: Use explicit names or prefixes:
@route("api", name="create_user")
def handle_create_user(self): ...
@route("api", name="create_order")
def handle_create_order(self): ...
Plugins don’t propagate to children
Problem: Children don’t see parent plugins.
Solution: Make sure to attach plugins to the parent router before connecting children:
# CORRECT
self.api = Router(self, name="api").plug("logging")
self.attach_instance(child, name="child") # Child inherits logging
# WRONG
self.attach_instance(child, name="child")
self.api.plug("logging") # Child does NOT inherit
ValidationError with Pydantic
Problem: ValidationError even with correct input.
Solution: Verify:
PydanticPlugin attached:
router.plug("pydantic")Type hint correct:
def method(self, req: MyModel)Input is dict or model instance:
router.node("method")({"field": "value"})
Best Practices
When should I use Genro Routes?
✅ Use Genro Routes when:
You have many handlers to organize dynamically
You want to extend behavior with plugins
You need hierarchical routing (parent/child)
You need to expose handlers via multiple interfaces (CLI/HTTP/WS)
❌ Don’t use Genro Routes when:
You only have 2-3 simple methods (overkill)
You don’t need dynamic dispatch
You prefer explicit/static routing
Does plugin order matter?
Question: Is the order of .plug() important?
Answer: Yes. Plugins are applied in attachment order:
router.plug("logging").plug("pydantic")
# Execution: logging → pydantic → handler → pydantic → logging
Outer logging sees everything, inner Pydantic validates.
How do I test code with Genro Routes?
Question: How do I write tests for handlers with Genro Routes?
Answer: Test directly or via router:
# Direct test
def test_handler_logic():
obj = MyClass()
assert obj.my_handler({"input": "test"}) == expected
# Test via router
def test_router_integration():
obj = MyClass()
node = obj.api.node("my_handler")
assert node({"input": "test"}) == expected
Useful Links
Quick Start - Get started in 5 minutes
Basic Usage - Fundamental concepts
Execution Context - RoutingContext, parent chain, slot-based ctx
Plugin Guide - Plugin development
Hierarchies - Nested routing
API Reference - Complete documentation
Contributing
Have more questions? Open an issue or contribute to this FAQ!