Basic Usage
This guide covers SmartRoute’s core features with practical examples derived from the test suite.
Overview
SmartRoute provides instance-scoped routing with hierarchical organization and plugin support. Each router instance is independent with its own plugin state.
Key concepts:
Routers are instantiated at runtime:
Router(self, name="api")Methods are marked with
@route("router_name")decoratorEach instance gets isolated routing state
Plugins apply per-instance, not globally
Creating Your First Router
Create a service with instance-scoped routing:
from smartroute import RoutedClass, Router, route
class Service(RoutedClass):
def __init__(self, label: str):
self.label = label
self.api = Router(self, name="api")
@route("api")
def describe(self):
return f"service:{self.label}"
# Each instance is isolated
first = Service("alpha")
second = Service("beta")
assert first.api.get("describe")() == "service:alpha"
assert second.api.get("describe")() == "service:beta"
Key points:
Router(self, name="api")creates instance-scoped router in__init__@route("api")marks method for registrationRoutedClassmixin enables automatic router discovery and method registrationEach instance has independent routing state
Registering Handlers
Methods are automatically registered when decorated with @route:
class API(RoutedClass):
def __init__(self):
self.routes = Router(self, name="routes")
@route("routes")
def echo(self, value: str):
return value
@route("routes", name="alt_name")
def action(self):
return "executed"
api = API()
# Direct name resolution
assert api.routes.get("echo")("hello") == "hello"
# Custom name resolution
assert api.routes.get("alt_name")() == "executed"
Registration happens automatically when you inherit from RoutedClass and instantiate routers in __init__.
Calling Handlers
Use get() to retrieve handlers and call() for direct invocation:
class Calculator(RoutedClass):
def __init__(self):
self.ops = Router(self, name="ops")
@route("ops")
def add(self, a: int, b: int):
return a + b
calc = Calculator()
# Via get() - returns callable
handler = calc.ops.get("add")
assert handler(2, 3) == 5
# Via call() - invokes directly
result = calc.ops.call("add", 10, 20)
assert result == 30
Difference:
get(name)returns the callable (for reuse)call(name, *args, **kwargs)invokes immediately
Using Prefixes and Custom Names
Clean up method names with prefixes and provide alternative names with the name option:
class SubService(RoutedClass):
def __init__(self, prefix: str):
self.prefix = prefix
self.routes = Router(self, name="routes", prefix="handle_")
@route("routes")
def handle_list(self):
return f"{self.prefix}:list"
@route("routes", name="detail")
def handle_detail(self, ident: int):
return f"{self.prefix}:detail:{ident}"
sub = SubService("users")
# Prefix stripped: "handle_list" → "list"
assert sub.routes.get("list")() == "users:list"
# Custom name used: "handle_detail" → "detail"
assert sub.routes.get("detail")(10) == "users:detail:10"
Benefits:
Prefixes keep method names organized in code
Explicit names provide cleaner external APIs
Router resolves both automatically
Default Handlers
Provide fallback handlers when routes don’t exist:
class Fallback(RoutedClass):
def __init__(self):
self.api = Router(self, name="api")
@route("api")
def known_action(self):
return "success"
fb = Fallback()
# Existing handler
assert fb.api.get("known_action")() == "success"
# Non-existing with default
default_fn = lambda: "fallback"
assert fb.api.get("missing", default=default_fn)() == "fallback"
# Without default raises KeyError
try:
fb.api.get("missing")()
except KeyError:
pass # Expected
Use defaults to:
Handle optional functionality gracefully
Provide “not found” handlers
Implement fallback behavior
Dynamic Handler Registration
Add handlers programmatically at runtime:
class Dynamic(RoutedClass):
def __init__(self):
self.api = Router(self, name="api")
# Register a lambda
self.api.add_entry("greet", lambda name: f"Hello, {name}")
dyn = Dynamic()
# Dynamic handler works immediately
assert dyn.api.get("greet")("World") == "Hello, World"
Use cases:
Plugin-provided handlers
Configuration-driven routing
Runtime service composition
Building Hierarchies
Create nested router structures with dotted path access:
class SubService(RoutedClass):
def __init__(self, prefix: str):
self.prefix = prefix
self.routes = Router(self, name="routes", prefix="handle_")
@route("routes")
def handle_list(self):
return f"{self.prefix}:list"
@route("routes", name="detail")
def handle_detail(self, ident: int):
return f"{self.prefix}:detail:{ident}"
class RootAPI(RoutedClass):
def __init__(self):
self.api = Router(self, name="api")
self.users = SubService("users")
self.products = SubService("products")
self.api.attach_instance(self.users, name="users")
self.api.attach_instance(self.products, name="products")
root = RootAPI()
# Access with dotted paths
assert root.api.get("users.list")() == "users:list"
assert root.api.get("products.detail")(5) == "products:detail:5"
Hierarchies enable:
Organized service composition
Logical grouping of related handlers
Namespace isolation
Introspection
Inspect router structure and registered handlers:
class Inspectable(RoutedClass):
def __init__(self):
self.api = Router(self, name="api")
self.child_service = SubService("child")
self.api.attach_instance(self.child_service, name="sub")
@route("api")
def action(self):
pass
insp = Inspectable()
# Get metadata (single source: members)
info = insp.api.members()
assert "action" in info["handlers"]
assert "sub" in info["children"]
# Plugins can expose extra filters (e.g. scopes/channels from PublishPlugin)
internal_info = insp.api.members(scopes="internal", channel="CLI")
# Filters likewise depend on plugin metadata
internal = insp.api.members(scopes="internal")
internal_cli = insp.api.members(scopes="internal", channel="CLI")
Use members() to:
Generate API documentation
Debug routing issues
Validate configuration
Next Steps
Now that you understand the basics:
Plugin Guide - Extend functionality with plugins
Hierarchies Guide - Advanced nested routing patterns
Best Practices - Production-ready patterns
API Reference - Complete API documentation