| Crates.io | this-rs |
| lib.rs | this-rs |
| version | 0.0.6 |
| created_at | 2025-10-22 14:29:22.832508+00 |
| updated_at | 2025-10-29 15:25:18.092878+00 |
| description | Framework for building complex multi-entity REST and GraphQL APIs with many relationships |
| homepage | |
| repository | https://github.com/this-rs/this |
| max_upload_size | |
| id | 1895745 |
| size | 900,403 |
A framework for building complex multi-entity REST and GraphQL APIs with many relationships in Rust.
Designed for APIs with 5+ entities and complex relationships.
For simple CRUD APIs, consider using Axum directly.
Example use cases: CMS, ERP, e-commerce platforms, social networks, project management tools
For simple projects, use Axum + utoipa directly.
📖 See Alternatives Comparison for detailed analysis of when to use what.
| Entities | Relationships | Recommended | Time Saved |
|---|---|---|---|
| 1-3 | Few | ❌ Axum directly | - |
| 3-5 | Some | ⚠️ Consider This-RS | ~20% |
| 5-10 | Many | ✅ This-RS recommended | ~40% |
| 10+ | Complex | ✅✅ This-RS highly recommended | ~60% |
// For each entity, you write:
// 1. Entity definition (✓ same in both)
// 2. CRUD handlers (✓ same in both - you still write business logic)
// 3. Routes registration (❌ REPETITIVE - 30+ lines per entity)
// 4. Link routes (❌ REPETITIVE - 50+ lines per relationship)
// 5. Link enrichment (❌ MANUAL - N+1 queries if not careful)
// 6. GraphQL schema (❌ MANUAL - duplicate type definitions)
// Example: 10 entities with 15 relationships
// = ~500 lines of repetitive routing code
// 1. Entity definition (✓ with macro helpers)
impl_data_entity!(Product, "product", ["name", "sku"], {
sku: String,
price: f64,
});
// 2. CRUD handlers (✓ you still write these - it's your business logic)
// 3. Routes registration (✅ AUTO-GENERATED)
// 4. Link routes (✅ AUTO-GENERATED from YAML)
// 5. Link enrichment (✅ AUTOMATIC - no N+1 queries)
// 6. GraphQL schema (✅ AUTO-GENERATED from entities)
// Main.rs for 10 entities with 15 relationships
let app = ServerBuilder::new()
.register_module(module)? // ← ~40 lines total
.build()?;
What you save: Routing boilerplate, link management, GraphQL schema duplication.
What you still write: Business logic handlers (as you should!).
use this::prelude::*;
// Macro generates full entity with all base fields
impl_data_entity!(Product, "product", ["name", "sku"], {
sku: String,
price: f64,
description: Option<String>,
stock: i32,
});
// Automatically includes:
// - id: Uuid (auto-generated)
// - type: String (auto-set to "product")
// - name: String (required)
// - created_at: DateTime<Utc> (auto-generated)
// - updated_at: DateTime<Utc> (auto-managed)
// - deleted_at: Option<DateTime<Utc>> (soft delete)
// - status: String (required)
use this::prelude::*;
#[derive(Clone)]
pub struct ProductStore {
data: Arc<RwLock<HashMap<Uuid, Product>>>,
}
// Implement EntityFetcher for link enrichment
#[async_trait]
impl EntityFetcher for ProductStore {
async fn fetch_as_json(&self, entity_id: &Uuid) -> Result<serde_json::Value> {
let product = self.get(entity_id)
.ok_or_else(|| anyhow::anyhow!("Product not found"))?;
Ok(serde_json::to_value(product)?)
}
}
// Implement EntityCreator for automatic entity creation
#[async_trait]
impl EntityCreator for ProductStore {
async fn create_from_json(&self, entity_data: serde_json::Value) -> Result<serde_json::Value> {
let product = Product::new(
entity_data["name"].as_str().unwrap_or("").to_string(),
entity_data["status"].as_str().unwrap_or("active").to_string(),
entity_data["sku"].as_str().unwrap_or("").to_string(),
entity_data["price"].as_f64().unwrap_or(0.0),
entity_data["description"].as_str().map(String::from),
entity_data["stock"].as_i64().unwrap_or(0) as i32,
);
self.add(product.clone());
Ok(serde_json::to_value(product)?)
}
}
impl Module for CatalogModule {
fn name(&self) -> &str { "catalog-service" }
fn entity_types(&self) -> Vec<&str> { vec!["product"] }
fn links_config(&self) -> Result<LinksConfig> {
LinksConfig::from_file("config/links.yaml")
}
fn register_entities(&self, registry: &mut EntityRegistry) {
registry.register(Box::new(ProductDescriptor::new(self.store.clone())));
}
fn get_entity_fetcher(&self, entity_type: &str) -> Option<Arc<dyn EntityFetcher>> {
match entity_type {
"product" => Some(Arc::new(self.store.clone()) as Arc<dyn EntityFetcher>),
_ => None,
}
}
fn get_entity_creator(&self, entity_type: &str) -> Option<Arc<dyn EntityCreator>> {
match entity_type {
"product" => Some(Arc::new(self.store.clone()) as Arc<dyn EntityCreator>),
_ => None,
}
}
}
#[tokio::main]
async fn main() -> Result<()> {
let app = ServerBuilder::new()
.with_link_service(InMemoryLinkService::new())
.register_module(CatalogModule::new(store))?
.build()?; // ← All REST routes created automatically!
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, app).await?;
Ok(())
}
use this::server::GraphQLExposure;
#[tokio::main]
async fn main() -> Result<()> {
let host = ServerBuilder::new()
.with_link_service(InMemoryLinkService::new())
.register_module(CatalogModule::new(store))?
.build_host()?; // ← Build transport-agnostic host
let graphql_app = GraphQLExposure::build_router(Arc::new(host))?;
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await?;
axum::serve(listener, graphql_app).await?;
Ok(())
}
That's it! Routes are auto-generated:
REST API:
GET /products - List allPOST /products - Create new productGET /products/:id - Get by IDPUT /products/:id - Update productDELETE /products/:id - Delete productGET /products/:id/links - IntrospectionGraphQL API:
POST /graphql - GraphQL endpoint with full CRUDGET /graphql/playground - Interactive GraphQL playgroundGET /graphql/schema - Dynamic schema introspectionProduct, Order, etc.)products, product(id))createProduct, updateProduct, deleteProduct)# POST /orders/{order_id}/invoices/{invoice_id}
curl -X POST http://localhost:3000/orders/abc-123/invoices/inv-456 \
-H 'Content-Type: application/json' \
-d '{"metadata": {"priority": "high"}}'
# POST /orders/{order_id}/invoices
curl -X POST http://localhost:3000/orders/abc-123/invoices \
-H 'Content-Type: application/json' \
-d '{
"entity": {
"number": "INV-999",
"amount": 1500.00,
"status": "active"
},
"metadata": {"priority": "high"}
}'
# Response includes both created entity AND link!
{
"entity": {
"id": "inv-999-uuid",
"type": "invoice",
"name": "INV-999",
"amount": 1500.00,
...
},
"link": {
"id": "link-uuid",
"source_id": "abc-123",
"target_id": "inv-999-uuid",
...
}
}
When you query links, you automatically get full entity data:
# GET /orders/{id}/invoices
{
"links": [
{
"id": "link-123",
"source_id": "order-abc",
"target_id": "invoice-xyz",
"target": {
"id": "invoice-xyz",
"type": "invoice",
"name": "INV-001",
"amount": 1500.00,
...
}
}
]
}
No N+1 queries! Entities are fetched efficiently in the background.
Complete billing microservice with auto-generated routes for both REST and GraphQL:
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
🔗 Link Routes (Auto-generated):
GET /orders/{id}/invoices - List invoices for an order
POST /orders/{id}/invoices - Create new invoice + link automatically
POST /orders/{id}/invoices/{inv_id} - Link existing order & invoice
PUT /orders/{id}/invoices/{inv_id} - Update link metadata
DELETE /orders/{id}/invoices/{inv_id} - Delete link
See examples/microservice/README.md for full REST API details.
cargo run --example microservice_graphql --features graphql
The same entities are exposed via GraphQL with:
Order, Invoice, Payment) auto-generatedorder.invoices, invoice.payments automaticallyhttp://127.0.0.1:3000/graphql/playgroundExample query:
query {
orders {
id
number
customerName
amount
invoices {
id
number
amount
payments {
id
amount
method
}
}
}
}
See examples/microservice/README_GRAPHQL.md for full GraphQL details.
Entity (Base Trait)
├── id: Uuid
├── type: String
├── created_at: DateTime<Utc>
├── updated_at: DateTime<Utc>
├── deleted_at: Option<DateTime<Utc>>
└── status: String
├─► Data (Inherits Entity)
│ └── name: String
│ + indexed_fields()
│ + field_value()
│
└─► Link (Inherits Entity)
├── source_id: Uuid
├── target_id: Uuid
└── link_type: String
impl_data_entity! - Generate a complete Data entityimpl_link_entity! - Generate a custom Link entityentity_fields! - Inject base Entity fieldsdata_fields! - Inject Entity + name fieldslink_fields! - Inject Entity + link fields✅ -88% less routing boilerplate (340 → 40 lines in microservice example)
✅ Add entity quickly - Macro helpers + module registration
✅ Consistent patterns - Same structure for all entities
✅ Type-safe - Full Rust compile-time checks
✅ Scales well - Adding the 10th entity is as easy as the 1st
✅ Multi-protocol - 🆕 Same entities exposed via REST and GraphQL
⚠️ Learning curve - Framework abstractions to understand (traits, registry)
✅ Faster development - Less code to write and maintain
✅ Easier onboarding - Clear patterns and conventions
✅ Reduced errors - Less manual work = fewer mistakes
✅ Better consistency - Framework enforces best practices
✅ Flexible APIs - 🆕 Choose REST, GraphQL, or both
✅ Authorization - Declarative auth policies
✅ Configurable - YAML-based configuration
✅ Extensible - Plugin architecture via modules
✅ Performance - Efficient link enrichment with no N+1 queries
✅ Soft Deletes - Built-in soft delete support
✅ Dynamic Schema - 🆕 GraphQL schema auto-generated from entities
Contributions are welcome! Please feel free to submit a Pull Request.
This project is licensed under the MIT License - see the LICENSE-MIT file for details.
"The best code is the code you don't have to write... if you're writing it 50 times."
This-RS eliminates repetitive routing and relationship boilerplate while maintaining type safety.
Perfect for:
NOT ideal for:
Built with Rust. Designed for complex APIs. Best for scale. 🦀✨
Made with ❤️ and 🦀 by the This-RS community