The CRUDResource Trait
CRUDResource is the core trait that powers all CRUD operations. CRUDCrate generates its implementation, but understanding it helps you customize behavior.
Trait Definition
pub trait CRUDResource: Sized + Send + Sync {
/// The Sea-ORM entity type
type EntityType: EntityTrait;
/// Model for creating new records
type CreateModel: DeserializeOwned + Send + Sync;
/// Model for updating existing records
type UpdateModel: DeserializeOwned + Send + Sync;
/// Model for list responses (can differ from Self)
type ListModel: Serialize + Send + Sync;
/// The primary key type
type PrimaryKey: Send + Sync;
/// Get a single record by ID
async fn get_one<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Self::PrimaryKey,
) -> Result<Self, ApiError>;
/// Get all records with filtering, sorting, and pagination
async fn get_all<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
condition: Condition,
order: (Self::EntityType::Column, Order),
offset: u64,
limit: u64,
) -> Result<Vec<Self::ListModel>, ApiError>;
/// Create a new record
async fn create<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
data: Self::CreateModel,
) -> Result<Self, ApiError>;
/// Update an existing record
async fn update<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Self::PrimaryKey,
data: Self::UpdateModel,
) -> Result<Self, ApiError>;
/// Delete a record
async fn delete<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Self::PrimaryKey,
) -> Result<(), ApiError>;
/// Delete multiple records
async fn delete_many<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
ids: Vec<Self::PrimaryKey>,
) -> Result<u64, ApiError>;
/// Get total count matching condition
async fn total_count<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
condition: &Condition,
) -> u64;
}
Generated Implementation
For this entity:
#[derive(EntityToModels)]
#[sea_orm(table_name = "tasks")]
pub struct Model {
#[crudcrate(primary_key)]
pub id: i32,
pub title: String,
}
CRUDCrate generates:
impl CRUDResource for Task {
type EntityType = Entity;
type CreateModel = TaskCreate;
type UpdateModel = TaskUpdate;
type ListModel = TaskList;
type PrimaryKey = i32;
async fn get_one<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: i32,
) -> Result<Self, ApiError> {
let model = Entity::find_by_id(id)
.one(db)
.await
.map_err(ApiError::from)?
.ok_or(ApiError::NotFound)?;
Ok(model.into())
}
async fn get_all<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
condition: Condition,
order: (Column, Order),
offset: u64,
limit: u64,
) -> Result<Vec<TaskList>, ApiError> {
let models = Entity::find()
.filter(condition)
.order_by(order.0, order.1)
.offset(offset)
.limit(limit)
.all(db)
.await
.map_err(ApiError::from)?;
Ok(models.into_iter().map(|m| m.into()).collect())
}
async fn create<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
data: TaskCreate,
) -> Result<Self, ApiError> {
let active_model: ActiveModel = data.into();
let model = active_model.insert(db).await.map_err(ApiError::from)?;
Ok(model.into())
}
async fn update<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: i32,
data: TaskUpdate,
) -> Result<Self, ApiError> {
let existing = Entity::find_by_id(id)
.one(db)
.await
.map_err(ApiError::from)?
.ok_or(ApiError::NotFound)?;
let mut active_model: ActiveModel = existing.into();
data.merge_into(&mut active_model);
let model = active_model.update(db).await.map_err(ApiError::from)?;
Ok(model.into())
}
async fn delete<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: i32,
) -> Result<(), ApiError> {
let result = Entity::delete_by_id(id)
.exec(db)
.await
.map_err(ApiError::from)?;
if result.rows_affected == 0 {
return Err(ApiError::NotFound);
}
Ok(())
}
async fn delete_many<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
ids: Vec<i32>,
) -> Result<u64, ApiError> {
// Safety limit: max 100 items per request
if ids.len() > 100 {
return Err(ApiError::BadRequest(
"Cannot delete more than 100 items at once".into()
));
}
let result = Entity::delete_many()
.filter(Column::Id.is_in(ids))
.exec(db)
.await
.map_err(ApiError::from)?;
Ok(result.rows_affected)
}
async fn total_count<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
condition: &Condition,
) -> u64 {
Entity::find()
.filter(condition.clone())
.count(db)
.await
.unwrap_or(0)
}
}
Using CRUDResource Directly
You can call trait methods directly:
use crudcrate::CRUDResource;
// Get one
let task = Task::get_one(&db, 42).await?;
// Get all with filtering
let condition = Condition::all()
.add(Column::Status.eq("active"));
let tasks = Task::get_all(&db, condition, (Column::Id, Order::Asc), 0, 10).await?;
// Create
let new_task = Task::create(&db, TaskCreate {
title: "New task".into(),
}).await?;
// Update
let updated = Task::update(&db, 42, TaskUpdate {
title: Some("Updated title".into()),
}).await?;
// Delete
Task::delete(&db, 42).await?;
// Count
let count = Task::total_count(&db, &Condition::all()).await;
Relationship Loading
When entities have relationships, get_one loads them:
// Entity with relationship
#[sea_orm(ignore)]
#[crudcrate(non_db_attr, join(one))]
pub comments: Vec<Comment>,
// Generated get_one includes relationship loading
async fn get_one<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(db: &C, id: i32) -> Result<Self, ApiError> {
let model = Entity::find_by_id(id)
.one(db)
.await?
.ok_or(ApiError::NotFound)?;
// Load related comments
let comments = model
.find_related(comment::Entity)
.all(db)
.await?
.into_iter()
.map(|c| c.into())
.collect();
Ok(Task {
id: model.id,
title: model.title,
comments, // Loaded!
})
}
Custom Operations with CRUDOperations
Extend behavior without reimplementing the trait:
#[derive(EntityToModels)]
#[crudcrate(generate_router, operations = TaskOperations)]
pub struct Model { /* ... */ }
pub struct TaskOperations;
impl CRUDOperations for TaskOperations {
type Resource = Task;
/// Called before create
async fn before_create<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
&self,
db: &C,
data: &mut TaskCreate,
) -> Result<(), ApiError> {
// Validate or transform
data.title = data.title.trim().to_string();
Ok(())
}
/// Called after create
async fn after_create<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
&self,
db: &C,
created: &Task,
) -> Result<(), ApiError> {
// Send notification, update cache, etc.
Ok(())
}
/// Called before delete
async fn before_delete<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
&self,
db: &C,
id: i32,
) -> Result<(), ApiError> {
// Check permissions, cascade deletes, etc.
Ok(())
}
}
Handler Integration
The generated router uses CRUDResource methods:
// Generated handler (simplified)
async fn get_handler(
Path(id): Path<i32>,
Extension(db): Extension<DatabaseConnection>,
) -> Result<Json<Task>, ApiError> {
let item = Task::get_one(&db, id).await?;
Ok(Json(item))
}
async fn list_handler(
Query(params): Query<FilterOptions>,
Extension(db): Extension<DatabaseConnection>,
) -> Result<(HeaderMap, Json<Vec<TaskList>>), ApiError> {
let condition = apply_filters::<Entity>(¶ms)?;
let (offset, limit) = parse_pagination(¶ms);
let order = parse_sorting::<Entity>(¶ms);
let items = Task::get_all(&db, condition, order, offset, limit).await?;
let total = Task::total_count(&db, &condition).await;
let headers = calculate_content_range("tasks", offset, items.len(), total);
Ok((headers, Json(items)))
}
Type Associations
The trait’s associated types connect everything:
impl CRUDResource for Task {
// Links to Sea-ORM entity for database operations
type EntityType = Entity;
// Request model for POST /tasks
type CreateModel = TaskCreate;
// Request model for PUT /tasks/:id
type UpdateModel = TaskUpdate;
// Response model for GET /tasks
type ListModel = TaskList;
// Type of the primary key
type PrimaryKey = i32;
}
This enables:
- Type-safe column references (
Entity::Column) - Correct serialization/deserialization
- Compile-time verification of operations
Error Handling
All methods return Result<T, ApiError>:
async fn get_one<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(db: &C, id: i32) -> Result<Self, ApiError> {
Entity::find_by_id(id)
.one(db)
.await
.map_err(ApiError::from)? // Database error → 500
.ok_or(ApiError::NotFound) // Not found → 404
}
Database errors are logged internally but return sanitized messages to clients.
Next Steps
- Learn about Attributes for configuration
- Implement Custom Operations
- Configure Error Handling