How does this-rs achieve automatic route generation?
this-rs uses a two-tier routing system: entity-specific routes (declared per entity) and generic link routes (fully automatic). Hereβs how it works.
Each entity declares its own CRUD routes via its EntityDescriptor:
// entities/order/descriptor.rs
impl EntityDescriptor for OrderDescriptor {
fn build_routes(&self) -> Router {
Router::new()
.route("/orders", get(list_orders).post(create_order))
.route("/orders/{id}",
get(get_order)
.put(update_order)
.delete(delete_order))
.with_state(state)
}
}
Why entity-specific?
Link routes are completely generic and work for all entities:
// src/server/router.rs
pub fn build_link_routes(state: AppState) -> Router {
Router::new()
.route("/links/{link_id}", get(get_link))
.route(
"/{entity_type}/{entity_id}/{route_name}",
get(list_links).post(create_linked_entity),
)
.route(
"/{source_type}/{source_id}/{route_name}/{target_id}",
get(get_link_by_route)
.post(create_link)
.put(update_link)
.delete(delete_link),
)
.route(
"/{entity_type}/{entity_id}/links",
get(list_available_links),
)
.with_state(state)
}
Why generic?
// ServerBuilder.build()
pub fn build(self) -> Result<Router> {
// 1. Build entity-specific routes
let entity_routes = self.entity_registry.build_routes();
// Calls OrderDescriptor.build_routes()
// Calls InvoiceDescriptor.build_routes()
// Calls PaymentDescriptor.build_routes()
// 2. Build generic link routes
let link_routes = build_link_routes(link_state);
// Single set of routes for ALL entities
// 3. Merge both
Ok(entity_routes.merge(link_routes))
}
Entity Routes (via EntityDescriptor):
GET /orders β OrderDescriptor
POST /orders β OrderDescriptor
GET /orders/{id} β OrderDescriptor
PUT /orders/{id} β OrderDescriptor
DELETE /orders/{id} β OrderDescriptor
GET /invoices β InvoiceDescriptor
POST /invoices β InvoiceDescriptor
... (same for all entities)
Link Routes (generic, works for all):
GET /{entity}/{id}/{route_name}
POST /{entity}/{id}/{route_name}
GET /{entity}/{id}/{route_name}/{target_id}
POST /{entity}/{id}/{route_name}/{target_id}
PUT /{entity}/{id}/{route_name}/{target_id}
DELETE /{entity}/{id}/{route_name}/{target_id}
Approach 1: Generic Handlers with match
pub async fn generic_list(
Path(entity_type): Path<String>,
) -> Result<Response, StatusCode> {
match entity_type.as_str() {
"orders" => /* call order store */,
"invoices" => /* call invoice store */,
"payments" => /* call payment store */,
_ => Err(StatusCode::NOT_FOUND),
}
}
β Problems:
Approach 2: Trait-based Dynamic Dispatch
pub trait CrudService<T>: Send + Sync {
async fn list(&self) -> Result<Vec<T>>;
async fn create(&self, entity: T) -> Result<T>;
// ...
}
// Generic handler using trait
pub async fn generic_list(
Path(entity_type): Path<String>,
State(services): State<HashMap<String, Arc<dyn CrudService<???>>>>
) -> Result<Response, StatusCode> {
// Problem: Can't use dyn CrudService<T> with different T types in same HashMap!
}
β Problems:
β Entity Routes: Declared per entity (type-safe, flexible) β Link Routes: Fully generic (zero boilerplate)
Why it works:
Entity CRUD:
Link Management:
The EntityDescriptor pattern gives us:
// Adding a new entity = just implement EntityDescriptor
impl EntityDescriptor for ProductDescriptor {
fn build_routes(&self) -> Router {
Router::new()
.route("/products", get(list).post(create))
.route("/products/{id}", get(get_one).put(update).delete(delete))
.with_state(state)
}
}
// Register it
module.register_entities(registry);
// Done! Routes are auto-generated when server builds
The LinkRouteRegistry enables semantic URLs:
# config/links.yaml
links:
- link_type: has_invoice
source_type: order
target_type: invoice
forward_route_name: invoices # β User-friendly name
reverse_route_name: order
# User accesses:
GET /orders/123/invoices
# Framework resolves:
route_name="invoices" + source_type="order"
β LinkDefinition { link_type: "has_invoice", target_type: "invoice", ... }
β Query: find links where source_id=123 and link_type="has_invoice"
| Aspect | Entity Routes | Link Routes |
|---|---|---|
| Declaration | Per entity (EntityDescriptor) | Generic (one set for all) |
| Type safety | Full compile-time | Runtime with validation |
| Customization | Easy per entity | Configuration-driven |
| Boilerplate | ~20 lines per entity | 0 lines |
| Performance | Direct function calls | Registry lookup + dispatch |
| Flexibility | High (entity-specific logic) | High (YAML configuration) |
this-rs uses a hybrid approach:
EntityDescriptor
Result: Best of both worlds - type safety where needed, zero boilerplate where possible! ππ¦β¨