Complete example of a billing microservice managing the Order β Invoice β Payment workflow, demonstrating:
Module traitThis microservice uses the frameworkβs ServerBuilder to auto-generate all routes:
#[tokio::main]
async fn main() -> Result<()> {
let entity_store = EntityStore::new();
let module = BillingModule::new(entity_store);
// β¨ All routes are auto-generated here!
let app = ServerBuilder::new()
.with_link_service(InMemoryLinkService::new())
.register_module(module)?
.build()?;
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
Zero manual routing lines needed! All CRUD and link routes are created automatically.
microservice/
βββ config/ # Externalized configuration
β βββ links.yaml # Entity, link, and auth configuration
βββ store.rs # Aggregated store (access to individual stores)
βββ main.rs # Entry point (~150 lines including 100 lines of test data)
βββ module.rs # BillingModule (implements Module trait)
βββ entities/ # One folder per entity (best practice)
βββ mod.rs # Entity re-exports
βββ order/
β βββ mod.rs # Order module
β βββ model.rs # Order struct (uses macro!)
β βββ store.rs # OrderStore (in-memory)
β βββ handlers.rs # HTTP handlers (create, list, get, etc.)
β βββ descriptor.rs # OrderDescriptor (registers routes)
βββ invoice/
β βββ ... (same structure)
βββ payment/
βββ ... (same structure)
Order ββ(has_invoice)βββΊ Invoice ββ(has_payment)βββΊ Payment
β β β
βββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ
Complete billing workflow with links
cd this-rs
cargo run --example microservice
Output:
π Starting billing-service v1.0.0
π¦ Entities: ["order", "invoice", "payment"]
π Server running on http://127.0.0.1:3000
π Entity Routes (CRUD - Auto-generated):
GET /orders - List all orders
POST /orders - Create a new order
GET /orders/{id} - Get a specific order
PUT /orders/{id} - Update an order
DELETE /orders/{id} - Delete an order
GET /invoices - List all invoices
POST /invoices - Create a new invoice
... (same for payments)
π Link Routes (Generic for all entities):
GET /links/{link_id} - Get a specific link by ID
GET /{entity}/{id}/{route_name} - List links (e.g. /orders/123/invoices)
POST /{entity}/{id}/{route_name} - Create new entity + link automatically β
GET /{source}/{id}/{route_name}/{target_id} - Get a specific link
POST /{source}/{id}/{route_name}/{target_id} - Create link between existing entities
PUT /{source}/{id}/{route_name}/{target_id} - Update link metadata
DELETE /{source}/{id}/{route_name}/{target_id} - Delete a link
GET /{entity}/{id}/links - Introspection (list available link types)
π Specific Link Routes (from config):
GET /orders/{id}/invoices - List invoices for an order
POST /orders/{id}/invoices - Create new invoice + link β
GET /orders/{id}/invoices/{invoice_id} - Get specific orderβinvoice link
POST /orders/{id}/invoices/{invoice_id} - Link existing order & invoice
PUT /orders/{id}/invoices/{invoice_id} - Update orderβinvoice link
DELETE /orders/{id}/invoices/{invoice_id} - Delete orderβinvoice link
GET /invoices/{id}/order - Get order for an invoice
GET /invoices/{id}/payments - List payments for an invoice
POST /invoices/{id}/payments - Create new payment + link β
GET /invoices/{id}/payments/{payment_id} - Get specific invoiceβpayment link
POST /invoices/{id}/payments/{payment_id} - Link existing invoice & payment
GET /payments/{id}/invoice - Get invoice for a payment
# Create an order
curl -X POST http://localhost:3000/orders \
-H 'Content-Type: application/json' \
-d '{
"number": "ORD-123",
"amount": 1500.00,
"customer_name": "Acme Corp",
"status": "active"
}'
# Create an invoice
curl -X POST http://localhost:3000/invoices \
-H 'Content-Type: application/json' \
-d '{
"number": "INV-456",
"amount": 1500.00,
"due_date": "2024-12-31",
"status": "pending"
}'
# Link existing order and invoice
curl -X POST http://localhost:3000/orders/{order_id}/invoices/{invoice_id} \
-H 'Content-Type: application/json' \
-d '{
"metadata": {
"note": "Standard invoice",
"priority": "high",
"created_by": "system"
}
}'
# Create a NEW invoice and link it to the order in one call!
curl -X POST http://localhost:3000/orders/{order_id}/invoices \
-H 'Content-Type: application/json' \
-d '{
"entity": {
"number": "INV-999",
"amount": 999.99,
"due_date": "2024-12-31",
"status": "active"
},
"metadata": {
"note": "Auto-created invoice",
"priority": "high"
}
}'
# Response includes BOTH the created invoice AND the link!
{
"entity": {
"id": "invoice-uuid",
"type": "invoice",
"name": "INV-999",
"number": "INV-999",
"amount": 999.99,
"created_at": "2024-10-23T...",
...
},
"link": {
"id": "link-uuid",
"type": "link",
"link_type": "has_invoice",
"source_id": "order-uuid",
"target_id": "invoice-uuid",
"created_at": "2024-10-23T...",
...
}
}
# List invoices for an order (includes full invoice data!)
curl http://localhost:3000/orders/{order_id}/invoices | jq .
# Response with enriched entities:
{
"links": [
{
"id": "link-123",
"type": "link",
"link_type": "has_invoice",
"source_id": "order-uuid",
"target_id": "invoice-uuid",
"target": {
"id": "invoice-uuid",
"type": "invoice",
"name": "INV-001",
"number": "INV-001",
"amount": 1500.00,
"due_date": "2024-12-31",
...
},
"metadata": {
"note": "Standard invoice",
"priority": "high"
}
}
],
"count": 1,
"link_type": "has_invoice",
"direction": "Forward"
}
# Get a specific link (includes both order and invoice!)
curl http://localhost:3000/orders/{order_id}/invoices/{invoice_id} | jq .
# Get order from an invoice (reverse navigation)
curl http://localhost:3000/invoices/{invoice_id}/order | jq .
# Update link metadata
curl -X PUT http://localhost:3000/orders/{order_id}/invoices/{invoice_id} \
-H 'Content-Type: application/json' \
-d '{
"metadata": {
"status": "verified",
"verified_by": "admin",
"verified_at": "2024-10-23T12:00:00Z"
}
}'
# Delete a link
curl -X DELETE http://localhost:3000/orders/{order_id}/invoices/{invoice_id}
# Update an entity
curl -X PUT http://localhost:3000/orders/{order_id} \
-H 'Content-Type: application/json' \
-d '{
"amount": 2000.00,
"notes": "Updated amount"
}'
// entities/order/model.rs
use this::prelude::*;
impl_data_entity!(Order, "order", ["name", "number"], {
number: String,
amount: f64,
customer_name: Option<String>,
notes: Option<String>,
});
// That's it! You get:
// - All base Entity fields (id, type, created_at, updated_at, deleted_at, status)
// - name field (from Data trait)
// - Your custom fields (number, amount, customer_name, notes)
// - Full trait implementations (Entity, Data)
// - Constructor: Order::new(...)
// - Utility methods: soft_delete(), touch(), set_status(), restore()
// entities/order/store.rs
#[async_trait]
impl EntityCreator for OrderStore {
async fn create_from_json(&self, entity_data: serde_json::Value) -> Result<serde_json::Value> {
let order = Order::new(
entity_data["number"].as_str().unwrap_or("ORD-000").to_string(),
entity_data["status"].as_str().unwrap_or("active").to_string(),
entity_data["number"].as_str().unwrap_or("ORD-000").to_string(),
entity_data["amount"].as_f64().unwrap_or(0.0),
entity_data["customer_name"].as_str().map(String::from),
entity_data["notes"].as_str().map(String::from),
);
self.add(order.clone());
Ok(serde_json::to_value(order)?)
}
}
// entities/order/store.rs
#[async_trait]
impl EntityFetcher for OrderStore {
async fn fetch_as_json(&self, entity_id: &Uuid) -> Result<serde_json::Value> {
let order = self.get(entity_id)
.ok_or_else(|| anyhow::anyhow!("Order not found: {}", entity_id))?;
Ok(serde_json::to_value(order)?)
}
}
// module.rs
impl Module for BillingModule {
fn name(&self) -> &str { "billing-service" }
fn entity_types(&self) -> Vec<&str> { vec!["order", "invoice", "payment"] }
fn links_config(&self) -> Result<LinksConfig> {
LinksConfig::from_file("examples/microservice/config/links.yaml")
}
fn register_entities(&self, registry: &mut EntityRegistry) {
registry.register(Box::new(OrderDescriptor::new(self.store.orders.clone())));
registry.register(Box::new(InvoiceDescriptor::new(self.store.invoices.clone())));
registry.register(Box::new(PaymentDescriptor::new(self.store.payments.clone())));
}
fn get_entity_fetcher(&self, entity_type: &str) -> Option<Arc<dyn EntityFetcher>> {
match entity_type {
"order" => Some(Arc::new(self.store.orders.clone()) as Arc<dyn EntityFetcher>),
"invoice" => Some(Arc::new(self.store.invoices.clone()) as Arc<dyn EntityFetcher>),
"payment" => Some(Arc::new(self.store.payments.clone()) as Arc<dyn EntityFetcher>),
_ => None,
}
}
fn get_entity_creator(&self, entity_type: &str) -> Option<Arc<dyn EntityCreator>> {
match entity_type {
"order" => Some(Arc::new(self.store.orders.clone()) as Arc<dyn EntityCreator>),
"invoice" => Some(Arc::new(self.store.invoices.clone()) as Arc<dyn EntityCreator>),
"payment" => Some(Arc::new(self.store.payments.clone()) as Arc<dyn EntityCreator>),
_ => None,
}
}
}
# config/links.yaml
entities:
- singular: order
plural: orders
- singular: invoice
plural: invoices
- singular: payment
plural: payments
links:
- link_type: has_invoice
source_type: order
target_type: invoice
forward_route_name: invoices
reverse_route_name: order
description: "Order has invoices"
- link_type: has_payment
source_type: invoice
target_type: payment
forward_route_name: payments
reverse_route_name: invoice
description: "Invoice has payments"
- 300+ lines of manual routing
- Duplicate CRUD logic per entity
- Manual link handling
- N+1 query problems
- Inconsistent patterns
- 40 lines in main.rs
- Zero routing code
- Automatic link enrichment
- No N+1 queries
- Consistent patterns everywhere
entities/ with the 5 filesconfig/links.yamlDataService and LinkService for your DBlinks.yamlThis microservice demonstrates production-ready patterns with zero boilerplate! ππ¦β¨