SmartRoute - Frequently Asked Questions
What is SmartRoute?
What problem does SmartRoute 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: SmartRoute 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 SmartRoute handles the rest.
Example:
from smartroute import RoutedClass, Router, route
class OrdersAPI(RoutedClass):
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.get("list")() # Calls list()
orders.api.get("create")({"name": "order-3"}) # Calls create()
SmartRoute vs function dictionary?
Question: Why not just use a dictionary {"list": self.list, "create": self.create}?
Answer: SmartRoute offers:
Plugin system: add logging, validation, audit without touching handlers
Hierarchies: organize routers in trees with
attach_instance()Metadata: each handler can have scopes, channels, configurations
Introspection:
router.members()anddescribe()to explore structureIsolation: each instance has its own router with independent plugins
For simple apps, a dictionary may suffice. For complex services, SmartRoute provides structure and extensibility.
Is SmartRoute a web framework?
Question: Does SmartRoute replace FastAPI/Flask?
Answer: No. SmartRoute 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 (Publisher)
Internal orchestrators
Service composition
Dynamic dashboards
You can use SmartRoute 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.get("method_name")→ callableApplies plugins: intercepts decoration and execution
Is isolated per instance: each object has its own router
class Service(RoutedClass):
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.get("info")() # "service:alpha"
s2.api.get("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 RoutedClass?
Question: Do I always need to inherit from RoutedClass?
Answer: Recommended but not required. RoutedClass provides:
obj.routedclassproxy to access all routersobj.routedclass.configure()for global configurationAutomatic router registry management
Without RoutedClass 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() to connect child instances:
class Dashboard(RoutedClass):
def __init__(self):
self.api = Router(self, name="api")
self.sales = SalesModule()
self.finance = FinanceModule()
# Attach child instances
self.api.attach_instance(self.sales, name="sales")
self.api.attach_instance(self.finance, name="finance")
dashboard = Dashboard()
# Access with dotted path
dashboard.api.get("sales.report")()
dashboard.api.get("finance.summary")()
How do I access child routers?
Question: Once connected, how do I call child handlers?
Answer: Use dotted path:
# Dotted path
dashboard.api.get("sales.report")()
# Or direct access
dashboard.sales.api.get("report")()
# Introspection
members = dashboard.api.members()
# {
# "handlers": {...},
# "children": {
# "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(RoutedClass):
def __init__(self):
self.api = Router(self, name="api").plug("logging", level="debug")
self.child_obj = Child()
self.api.attach_instance(self.child_obj, name="child")
# Child automatically inherits logging plugin
parent = Parent()
parent.api.get("child.method")() # Logs with level=debug
Plugin System
What are plugins?
Question: What is a plugin in SmartRoute 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
Scope: limit handler visibility by channel
How do I use built-in plugins?
Question: Does SmartRoute have ready-to-use plugins?
Answer: Yes, 2 built-in plugins:
1. LoggingPlugin - Automatic logging
router = Router(self, name="api").plug("logging", level="debug")
router.api.get("method")() # Auto-logs the call
2. PydanticPlugin - Input validation
from pydantic import BaseModel
class CreateRequest(BaseModel):
name: str
count: int
@route("api")
def create(self, req: CreateRequest):
return {"status": "created"}
router.plug("pydantic")
router.get("create")({"name": "test", "count": 5}) # OK
router.get("create")({"name": "test"}) # ValidationError
Scope/channel policies are provided by the SmartPublisher ecosystem plugin; see Scope and Channel below.
How do I configure plugins at runtime?
Question: I want to change plugin configuration after creating the router.
Answer: Use routedclass.configure():
# Global for all handlers
obj.routedclass.configure("api:logging", level="warning")
# For specific handler
obj.routedclass.configure("api:logging/create", enabled=False)
# With wildcards
obj.routedclass.configure("*:logging/*", level="debug")
# Query configuration
report = obj.routedclass.configure("?")
Can I create custom plugins?
Question: How do I write a custom plugin?
Answer: Inherit from BasePlugin and implement the hooks:
from smartroute.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("audit", AuditPlugin)
router = Router(self, name="api").plug("audit")
Scope and Channel
What are scope and channel?
Question: I don’t understand the difference between “scope” and “channel”.
Answer:
Scope = logical tag identifying visibility level (e.g.,
internal,public_read,admin)Channel = physical channel where the handler can be exposed (e.g.,
CLI,HTTP,WS,MCP)
For scope/channel rules, use the SmartPublisher ecosystem plugin (PublishPlugin).
Example:
from smartpublisher.smartroute_plugins.publish import PublishPlugin
from smartroute import Router, route
router = Router(self, name="api").plug("publish") # import registers the plugin
# Handler with "internal" scope exposed only on CLI
@route("api", metadata={"scopes": "internal"})
def debug_status(self):
return {"memory": "1GB", "cpu": "45%"}
router.publish.set_config(scope_channels={"internal": ["CLI"]})
# Filter by channel
cli_only = router.members(channel="CLI") # Includes debug_status
http_only = router.members(channel="HTTP") # Does NOT include debug_status
When should I use PublishPlugin?
Question: In which scenarios is the publication plugin useful?
Answer: Use PublishPlugin when:
Multi-channel exposure: same app exposed on CLI, HTTP, WebSocket
Security separation: internal vs public handlers
API versioning:
public_v1,public_v2with different channelsMCP integration: limit handlers visible to AI agents
Practical example:
# Publisher with CLI admin and HTTP public
from smartpublisher.smartroute_plugins.publish import PublishPlugin
router = Router(self, name="api").plug("publish")
@route("api", metadata={"scopes": "admin"})
def restart_service(self): # CLI only
...
@route("api", metadata={"scopes": "public"})
def get_status(self): # CLI + HTTP
...
# Configuration
router.publish.set_config(scope_channels={
"admin": ["CLI"],
"public": ["CLI", "HTTP"]
})
Advanced Use Cases
How do I register handlers dynamically?
Question: I want to add handlers at runtime, not just with decorators.
Answer: Use router.add_entry():
# Lambda handler
router.add_entry(lambda: "dynamic", name="dynamic_handler")
# External function
def external_func():
return "external"
router.add_entry(external_func, name="external")
# Register all marked methods (lazy registration)
router.add_entry("*")
How do I handle errors and defaults?
Question: What happens if I call a non-existent handler?
Answer: You can specify a default handler:
# Default handler
def not_found():
return {"error": "handler not found"}
router = Router(self, name="api", default_handler=not_found)
router.get("missing")() # Returns {"error": "handler not found"}
# Without default
router2 = Router(self, name="api2")
router2.get("missing")() # Raises KeyError
How do I use SmartAsync?
Question: I want to wrap handlers with SmartAsync for async execution.
Answer: Enable use_smartasync:
# Global for router
router = Router(self, name="api", use_smartasync=True)
# For single call
handler = router.get("method", use_smartasync=False) # Override
# Runtime override
result = router.get("method", use_smartasync=True)()
Note: SmartAsync must be installed separately.
How do I introspect the structure?
Question: I want to see all registered handlers and children.
Answer: Use members() or describe():
# Structure snapshot
members = router.members()
# {
# "handlers": {
# "list": {"func": <function>, "metadata": {...}},
# "create": {...}
# },
# "children": {
# "sales": {...}
# }
# }
# Full description
description = router.describe()
# {
# "name": "api",
# "prefix": None,
# "plugins": ["logging", "pydantic"],
# "methods": {...},
# "children": {...}
# }
# With filters
internal_only = router.members(scopes="internal")
cli_handlers = router.members(channel="CLI")
Comparisons
SmartRoute vs decorator dispatch?
Question: Why not use functools.singledispatch?
Answer:
singledispatch→ dispatch by type of first argumentSmartRoute → dispatch by string name with metadata, plugins, hierarchies
Different use cases: singledispatch for typed polymorphism, SmartRoute 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 connect children after attaching plugins:
# CORRECT
router.plug("logging")
router.attach_instance(child, name="child") # Child inherits logging
# WRONG
router.attach_instance(child, name="child")
router.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.get("method")({"field": "value"})
Best Practices
When should I use SmartRoute?
✅ Use SmartRoute when:
You have many handlers to organize dynamically
You want to extend behavior with plugins
You need hierarchical routing (parent/child)
You have multi-channel exposure (CLI/HTTP/WS)
❌ Don’t use SmartRoute 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 SmartRoute?
Question: How do I write tests for handlers with SmartRoute?
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()
handler = obj.api.get("my_handler")
assert handler({"input": "test"}) == expected
Useful Links
Quick Start - Get started in 5 minutes
Basic Usage - Fundamental concepts
Plugin Guide - Plugin development
Hierarchies - Nested routing
API Reference - Complete documentation
Contributing
Have more questions? Open an issue or contribute to this FAQ!