Quick Start
Get started with Genro Routes in 5 minutes.
Genro Routes is a transport-agnostic routing engine - you define your handlers once, then expose them via HTTP (using genro-asgi), CLI, or any other transport.
Installation
pip install genro-routes
Your First Router
Create a service with instance-scoped routing:
from genro_routes import RoutingClass, Router, route
class Service(RoutingClass):
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.node("describe")() == "service:alpha"
assert second.api.node("describe")() == "service:beta"
Key concept: Routers are instantiated in __init__ with Router(self, ...) - each instance gets its own isolated router.
Custom Entry Names
Use prefixes and explicit names for cleaner method registration:
class SubService(RoutingClass):
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.node("list")() == "users:list"
# Custom name used: "handle_detail" → "detail"
assert sub.routes.node("detail")(10) == "users:detail:10"
Single Router Default
When a class has exactly one router, @route() without arguments uses it automatically:
class Table(RoutingClass):
def __init__(self):
self.table = Router(self, name="table")
@route() # Uses the only router automatically
def add(self, data):
return f"added:{data}"
t = Table()
assert t.table.node("add")("x") == "added:x"
If the class has multiple routers, you must specify the router name explicitly.
Building Hierarchies
Create nested router structures:
class RootAPI(RoutingClass):
def __init__(self):
self.api = Router(self, name="api")
self.users = SubService("users")
self.products = SubService("products")
self.attach_instance(self.users, name="users")
self.attach_instance(self.products, name="products")
root = RootAPI()
# Access with path separator
assert root.api.node("users/list")() == "users:list"
assert root.api.node("products/detail")(5) == "products:detail:5"
Adding Plugins
Extend behavior with plugins. Built-in plugins (logging, pydantic, auth, env, openapi) are pre-registered.
class PluginService(RoutingClass):
def __init__(self):
self.api = Router(self, name="api").plug("logging")
@route("api")
def do_work(self):
return "ok"
svc = PluginService()
result = svc.api.node("do_work")() # Automatically logged
Validating Arguments
Use Pydantic for automatic validation:
class ValidateService(RoutingClass):
def __init__(self):
self.api = Router(self, name="api").plug("pydantic")
@route("api")
def concat(self, text: str, number: int = 1) -> str:
return f"{text}:{number}"
svc = ValidateService()
# Valid inputs
assert svc.api.node("concat")("hello", 3) == "hello:3"
assert svc.api.node("concat")("hi") == "hi:1"
# Invalid inputs raise ValidationError
# svc.api.node("concat")(123, "oops") # ValidationError!
Response Schemas
Return type annotations are automatically converted to JSON Schema. This enables bridges (MCP, OpenAPI) to expose typed response contracts:
from typing import TypedDict
class StatusResponse(TypedDict):
ok: bool
message: str
class HealthService(RoutingClass):
def __init__(self):
self.api = Router(self, name="api").plug("pydantic")
@route("api")
def health(self) -> StatusResponse:
return {"ok": True, "message": "running"}
svc = HealthService()
# Response schema is generated automatically
entry = svc.api._entries["health"]
schema = entry.metadata["pydantic"]["response_schema"]
# {"type": "object", "properties": {"ok": {"type": "boolean"}, "message": {"type": "string"}}, ...}
Supported types: TypedDict, dict[str, T], list[T], str, int, bool, and any type Pydantic can serialize. TypedDict requires Python 3.12+.
Execution Context
Handlers need access to shared state (database, user, session) without
knowing which adapter provides it. Use RoutingContext:
from genro_routes import RoutingClass, RoutingContext, Router, route
class OrderService(RoutingClass):
def __init__(self):
self.api = Router(self, name="api")
@route("api")
def list_orders(self):
return self.ctx.db.query("SELECT * FROM orders")
# Create a context and set it
ctx = RoutingContext()
ctx.db = my_database
svc = OrderService()
svc.ctx = ctx
svc.api.node("list_orders")() # handler reads self.ctx.db
Contexts can be layered with RoutingContext(parent=parent_ctx) — missing
attributes walk up the chain. The context is stored in a _ctx slot and
walks up the _routing_parent chain — children inherit it automatically.
See the Execution Context Guide for the full reference.
Next Steps
Now that you’ve learned the basics:
Basic Usage Guide - Detailed explanation of core concepts
Execution Context Guide - RoutingContext, parent chain, ContextVar
Plugin Guide - Learn to create custom plugins
Hierarchies Guide - Master nested routers
Best Practices - Production-ready patterns
API Reference - Complete API documentation
Need Help?
Examples: Check the examples directory
Issues: Report bugs on GitHub Issues