this

Explanation: How Routing Works in this-rs

🎯 Question

How does this-rs achieve automatic route generation?

πŸ“ Answer

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.


πŸ—οΈ Two Types of Routes

1. Entity CRUD Routes (Entity-Specific)

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?


πŸ”„ How It Works

ServerBuilder Assembly

// 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))
}

Result: Complete API

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}

πŸ€” Why Not Fully Generic CRUD Routes?

Challenges with Fully Generic CRUD

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:

Current Approach: Best of Both Worlds

βœ… Entity Routes: Declared per entity (type-safe, flexible) βœ… Link Routes: Fully generic (zero boilerplate)

Why it works:


πŸ’‘ Key Insights

1. Different Routes Have Different Needs

Entity CRUD:

Link Management:

2. EntityDescriptor Pattern

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

3. LinkRouteRegistry

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"

πŸ“Š Comparison

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)

🎯 Conclusion

this-rs uses a hybrid approach:

  1. Entity CRUD routes: Declared per entity via EntityDescriptor
    • Maintains type safety
    • Allows customization
    • Self-registering
  2. Link routes: Fully generic
    • Zero boilerplate
    • Configuration-driven
    • Works for all entities
  3. ServerBuilder: Combines both automatically
    • Collects entity routes from descriptors
    • Adds generic link routes
    • Merges into complete API

Result: Best of both worlds - type safety where needed, zero boilerplate where possible! πŸš€πŸ¦€βœ¨