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 routing.instance() to retrieve children

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