| Crates.io | crudcrate |
| lib.rs | crudcrate |
| version | 0.7.0 |
| created_at | 2025-02-04 14:21:18.664459+00 |
| updated_at | 2025-11-26 15:34:08.714282+00 |
| description | Rust traits and functions to aid in building CRUD APIs with Axum and Sea-ORM |
| homepage | |
| repository | https://github.com/evanjt/crudcrate |
| max_upload_size | |
| id | 1542000 |
| size | 317,868 |
Tired of writing boilerplate for your APIs? Frustrated that your API models look almost identical to your database models, but you have to maintain both? What if you could get a complete CRUD API running in minutes, then customize only the parts that need special handling?
crudcrate transforms your Sea-ORM entities into fully-featured REST APIs with one line of code.
use crudcrate::EntityToModels;
#[derive(EntityToModels)]
#[crudcrate(generate_router)]
pub struct Model {
#[crudcrate(primary_key, exclude(create, update))]
pub id: Uuid,
#[crudcrate(filterable, sortable)]
pub title: String,
#[crudcrate(filterable)]
pub completed: bool,
}
// That's it. You now have:
// - Complete CRUD endpoints (GET, POST, PUT, DELETE)
// - Auto-generated API models (Todo, TodoCreate, TodoUpdate, TodoList)
// - Filtering, sorting, and pagination
// - OpenAPI documentation
You've been here before:
Customer with id, name, email, created_atAnd you repeat this for every single entity in your application.
Let crudcrate handle the repetitive stuff:
#[derive(EntityToModels)]
#[crudcrate(generate_router)]
pub struct Customer {
#[crudcrate(primary_key, exclude(create, update), on_create = Uuid::new_v4())]
pub id: Uuid,
#[crudcrate(filterable, sortable)]
pub name: String,
#[crudcrate(filterable)]
pub email: String,
#[crudcrete(exclude(create, update), on_create = Utc::now())]
pub created_at: DateTime<Utc>,
}
// Just plug it in:
let app = Router::new()
.nest("/api/customers", Customer::router(&db));
What you get instantly:
GET /api/customers - List with filtering, sorting, paginationGET /api/customers/{id} - Get single customerPOST /api/customers - Create new customerPUT /api/customers/{id} - Update customerDELETE /api/customers/{id} - Delete customerCustomer, CustomerCreate, CustomerUpdate, CustomerList models?filter={"name_like":"John"}?sort=name&order=DESC or ?sort=["name","DESC"]?page=1&per_page=20 or ?range=[0,19] (React Admin)That's where crudcrate shines. You get the basics for free, but can override anything:
// Need custom validation or permissions?
#[crudcrate(fn_get_one = custom_get_one)]
pub struct Customer { /* ... */ }
async fn custom_get_one(db: &DatabaseConnection, id: Uuid) -> Result<Customer, DbErr> {
// Add your custom logic here
let customer = Entity::find_by_id(id)
.filter(Column::UserId.eq(current_user_id())) // Permission check
.one(db)
.await?
.ok_or(DbErr::RecordNotFound("Customer not found"))?;
// Add logging, caching, audit trails, etc.
log::info!("Customer {} accessed by user {}", id, current_user_id());
Ok(customer.into())
}
Override any operation: fn_get_one, fn_get_all, fn_create, fn_update, fn_delete, fn_delete_many
One entity becomes four specialized models:
#[derive(EntityToModels)]
pub struct Model {
pub id: Uuid,
pub title: String,
pub completed: bool,
pub secret_data: String, // Sensitive field
}
// Generated models:
pub struct Todo { // API responses (get_one)
pub id: Uuid,
pub title: String,
pub completed: bool,
// secret_data excluded - sensitive info never sent to clients
}
pub struct TodoCreate { // POST requests (excluded fields omitted)
pub title: String,
pub completed: bool,
// id and secret_data excluded automatically
}
pub struct TodoUpdate { // PUT requests (all fields optional)
pub title: Option<String>,
pub completed: Option<bool>,
// id excluded, secret_data excluded unless you override
}
pub struct TodoList { // List responses (can exclude expensive fields)
pub id: Uuid,
pub title: String,
pub completed: bool,
// secret_data excluded to avoid leaking sensitive info in lists
}
#[crudcrate(filterable, sortable, fulltext)]
pub title: String,
#[crudcrate(filterable)]
pub priority: i32,
Your users can now:
# Exact matches
GET /api/tasks?filter={"completed":false,"priority":3}
# Numeric ranges
GET /api/tasks?filter={"priority_gte":2,"priority_lte":5}
# Text search across all searchable fields
GET /api/tasks?filter={"q":"urgent review"}
# Combine filters
GET /api/tasks?filter={"completed":false,"priority_gte":3,"q":"urgent"}
Automatically load related data in API responses with full recursive support:
pub struct Customer {
pub id: Uuid,
pub name: String,
// Automatically load related vehicles in API responses
#[sea_orm(ignore)]
#[crudcrate(non_db_attr, join(one, all))]
pub vehicles: Vec<Vehicle>,
}
pub struct Vehicle {
pub id: Uuid,
pub make: String,
// Each vehicle automatically loads its parts and maintenance records
#[sea_orm(ignore)]
#[crudcrate(non_db_attr, join(one, all))]
pub parts: Vec<VehiclePart>,
#[sea_orm(ignore)]
#[crudcrate(non_db_attr, join(one, all))]
pub maintenance_records: Vec<MaintenanceRecord>,
}
Multi-level recursive loading works out of the box:
Join options:
join(one) - Load only in individual item responsesjoin(all) - Load only in list responsesjoin(one, all) - Load in both types of responsesjoin(one, all, depth = 2) - Custom depth guidance (default: unlimited)Sometimes certain fields shouldn't be in certain models:
// Password hash: never send to clients, never allow updates
#[crudcrate(exclude(one, create, update, list))]
pub password_hash: String,
// API keys: generate server-side, never expose in any response
#[crudcrate(exclude(one, create, update, list), on_create = generate_api_key())]
pub api_key: String,
// Internal notes: exclude from list (expensive) but show in detail view
#[crudcrate(exclude(list))]
pub internal_notes: String,
// Timestamps: manage automatically
#[crudcrate(exclude(create, update), on_create = Utc::now(), on_update = Utc::now())]
pub updated_at: DateTime<Utc>,
Exclusion options:
exclude(one) - Exclude from get_one responses (main API response)exclude(create) - Exclude from POST request modelsexclude(update) - Exclude from PUT request modelsexclude(list) - Exclude from list responsesexclude(one, list) - Exclude from both individual and list responsesexclude(create, update) - Exclude from both request modelscrudcrate isn't just a toy - it's built for real applications:
// Get performance recommendations for production
crudcrate::analyse_all_registered_models(&db, false).await;
Output:
HIGH Priority:
customers - Fulltext search on name/email without proper index
CREATE INDEX idx_customers_fulltext ON customers USING GIN (to_tsvector('english', name || ' ' || email));
MEDIUM Priority:
customers - Field 'email' is filterable but not indexed
CREATE INDEX idx_customers_email ON customers (email);
crudcrate includes several built-in security limits to protect your application from common attack vectors.
Default: 100 items per batch delete
The default delete_many implementation limits batch deletions to 100 items to prevent DoS attacks via resource exhaustion.
To increase this limit, provide a custom implementation:
#[crudcrate(fn_delete_many = custom_delete_many)]
async fn custom_delete_many(
db: &DatabaseConnection,
ids: Vec<Uuid>
) -> Result<Vec<Uuid>, DbErr> {
const MAX_SIZE: usize = 500; // Your custom limit
if ids.len() > MAX_SIZE {
return Err(DbErr::Custom(format!("Too many items")));
}
// Your implementation...
}
Default: Maximum depth of 5
Recursive joins are automatically capped at depth 5 to prevent:
// Shallow joins - load one level only
#[crudcrate(join(all, depth = 1))]
pub users: Vec<User>
// Medium depth - 3 levels
#[crudcrate(join(all, depth = 3))]
pub organization: Option<Organization>
// Maximum depth - defaults to 5 if unspecified
#[crudcrate(join(all))] // depth = 5
pub vehicles: Vec<Vehicle>
// Values > 5 are automatically capped to 5
#[crudcrate(join(all, depth = 10))] // Will be capped to 5
Compile-time warnings: If you specify depth > 5, you'll get a compile-time error informing you of the cap.
See SECURITY_AUDIT.md for complete security details.
cargo add crudcrate sea-orm axum
use axum::Router;
use crudcrate::EntityToModels;
use sea_orm::entity::prelude::*;
#[derive(EntityToModels)]
#[crudcrate(generate_router)]
pub struct Task {
#[crudcrate(primary_key, exclude(create, update), on_create = Uuid::new_v4())]
pub id: Uuid,
#[crudcrate(sortable, filterable)]
pub title: String,
#[crudcrate(filterable)]
pub completed: bool,
}
#[tokio::main]
async fn main() {
let db = sea_orm::Database::connect("sqlite::memory:").await.unwrap();
let app = Router::new()
.nest("/api/tasks", Task::router(&db));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
println!("🚀 API running on http://localhost:3000");
axum::serve(listener, app).await.unwrap();
}
That's it. You have a complete, production-ready CRUD API.
Run it:
cargo run
Test it:
# Create a task
curl -X POST http://localhost:3000/api/tasks \
-H "Content-Type: application/json" \
-d '{"title":"Build CRUD API","completed":false}'
# List all tasks
curl http://localhost:3000/api/tasks
# Get a specific task
curl http://localhost:3000/api/tasks/{id}
# Update a task
curl -X PUT http://localhost:3000/api/tasks/{id} \
-H "Content-Type: application/json" \
-d '{"completed":true}'
Perfect for:
Maybe not for:
# Minimal todo API
cargo run --example minimal
# Relationship loading demo
cargo run --example recursive_join
MIT License. See LICENSE for details.