Lifecycle Hooks
Configure operation hooks directly through attributes for simple cases.
Overview
Lifecycle hooks provide attribute-based customization without implementing CRUDOperations:
#[derive(EntityToModels)]
#[crudcrate(
generate_router,
create::one::pre = validate_article,
create::one::post = notify_subscribers,
update::one::pre = check_permissions,
delete::one::pre = archive_instead_of_delete,
)]
pub struct Model {
// ...
}
Hook Types
Pre Hooks (::pre)
Execute before the database operation:
#[crudcrate(create::one::pre = validate_article)]
// Hook function signature
async fn validate_article<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
data: &mut ArticleCreate,
) -> Result<(), ApiError> {
if data.title.is_empty() {
return Err(ApiError::ValidationFailed(vec![
ValidationError::new("title", "Title is required")
]));
}
Ok(())
}
Post Hooks (::post)
Execute after the database operation succeeds:
#[crudcrate(create::one::post = notify_subscribers)]
// Hook function signature
async fn notify_subscribers<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
created: &Article,
) -> Result<(), ApiError> {
send_notifications(created.author_id).await;
Ok(())
}
Body Replacement (::body)
Replace the entire operation logic:
#[crudcrate(delete::one::body = soft_delete)]
// Completely replaces the delete handler
async fn soft_delete<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
) -> Result<(), ApiError> {
// Instead of deleting, mark as deleted
let mut article: ActiveModel = Entity::find_by_id(id)
.one(db)
.await?
.ok_or(ApiError::NotFound)?
.into();
article.deleted_at = Set(Some(chrono::Utc::now()));
article.update(db).await?;
Ok(())
}
Transform Hooks (::transform)
Modify the result after the operation (or body replacement) but before the post hook:
#[crudcrate(read::one::transform = enrich_article)]
// Hook function signature
async fn enrich_article<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
article: Article, // Takes ownership
) -> Result<Article, ApiError> {
// Enrich, decorate, or transform the result
Ok(article)
}
Transform hooks take ownership of the result and return a modified version. Supported for all operations (create, read, update, delete) in both one and many cardinality.
For many operations, the transform receives and returns Vec<T>:
async fn transform_articles<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
articles: Vec<Article>,
) -> Result<Vec<Article>, ApiError> {
// Transform the batch
Ok(articles)
}
Available Operations
| Operation | Description |
|---|---|
create::one | Create single item (POST /items) |
create::many | Batch create (POST /items/batch) |
update::one | Update single item (PUT /items/:id) |
update::many | Batch update (PATCH /items/batch) |
delete::one | Delete single item (DELETE /items/:id) |
delete::many | Bulk delete (DELETE /items with body) |
read::one | Get single item (GET /items/:id) |
read::all | List items (GET /items) |
Function Signatures
Create Hooks
// Pre hook - can modify the input
async fn create_pre<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
data: &mut ArticleCreate,
) -> Result<(), ApiError>;
// Post hook - receives the created entity
async fn create_post<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
created: &Article,
) -> Result<(), ApiError>;
// Body replacement - full control
async fn create_body<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
data: ArticleCreate,
) -> Result<Article, ApiError>;
// Transform - modify the result
async fn create_transform<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
created: Article,
) -> Result<Article, ApiError>;
Update Hooks
// Pre hook
async fn update_pre<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
data: &mut ArticleUpdate,
) -> Result<(), ApiError>;
// Post hook
async fn update_post<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
updated: &Article,
) -> Result<(), ApiError>;
// Body replacement
async fn update_body<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
data: ArticleUpdate,
) -> Result<Article, ApiError>;
// Transform - modify the result
async fn update_transform<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
updated: Article,
) -> Result<Article, ApiError>;
Delete Hooks
// Pre hook
async fn delete_pre<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
) -> Result<(), ApiError>;
// Post hook
async fn delete_post<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
) -> Result<(), ApiError>;
// Body replacement
async fn delete_body<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
) -> Result<(), ApiError>;
Get Hooks
// Pre hook for get_one
async fn get_one_pre<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
) -> Result<(), ApiError>;
// Pre hook for get_all - can modify the condition
async fn get_all_pre<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
condition: &mut Condition,
) -> Result<(), ApiError>;
Examples
Input Validation
#[crudcrate(create::one::pre = validate_user)]
async fn validate_user<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
_db: &C,
data: &mut UserCreate,
) -> Result<(), ApiError> {
let mut errors = Vec::new();
// Email validation
if !data.email.contains('@') {
errors.push(ValidationError::new("email", "Invalid email format"));
}
// Password strength
if data.password.len() < 8 {
errors.push(ValidationError::new("password", "Must be at least 8 characters"));
}
if errors.is_empty() {
Ok(())
} else {
Err(ApiError::ValidationFailed(errors))
}
}
Password Hashing
#[crudcrate(create::one::pre = hash_password)]
async fn hash_password<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
_db: &C,
data: &mut UserCreate,
) -> Result<(), ApiError> {
// Hash the password before storing
data.password_hash = bcrypt::hash(&data.password, bcrypt::DEFAULT_COST)
.map_err(|_| ApiError::Internal("Failed to hash password".into()))?;
// Clear the plain password
data.password.clear();
Ok(())
}
Soft Delete
#[crudcrate(delete::one::body = soft_delete_article)]
async fn soft_delete_article<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
) -> Result<(), ApiError> {
let article = Entity::find_by_id(id)
.one(db)
.await?
.ok_or(ApiError::NotFound)?;
let mut active: ActiveModel = article.into();
active.deleted_at = Set(Some(chrono::Utc::now()));
active.update(db).await?;
Ok(())
}
Row-Level Security
#[crudcrate(get::all::pre = filter_by_tenant)]
async fn filter_by_tenant<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
_db: &C,
condition: &mut Condition,
) -> Result<(), ApiError> {
let tenant_id = get_current_tenant_id();
*condition = condition.clone().add(Column::TenantId.eq(tenant_id));
Ok(())
}
Audit Trail
#[crudcrate(
create::one::post = log_create,
update::one::post = log_update,
delete::one::post = log_delete,
)]
async fn log_create<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
created: &Article,
) -> Result<(), ApiError> {
save_audit_log(db, "CREATE", "Article", &created.id.to_string()).await
}
async fn log_update<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
updated: &Article,
) -> Result<(), ApiError> {
save_audit_log(db, "UPDATE", "Article", &updated.id.to_string()).await
}
async fn log_delete<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
) -> Result<(), ApiError> {
save_audit_log(db, "DELETE", "Article", &id.to_string()).await
}
Cascade Updates
#[crudcrate(update::one::post = update_search_index)]
async fn update_search_index<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
_db: &C,
updated: &Article,
) -> Result<(), ApiError> {
// Update search engine
search_client::update_document("articles", updated.id, updated).await;
Ok(())
}
Authorization Check
#[crudcrate(
update::one::pre = check_edit_permission,
delete::one::pre = check_delete_permission,
)]
async fn check_edit_permission<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
_data: &mut ArticleUpdate,
) -> Result<(), ApiError> {
let article = Entity::find_by_id(id).one(db).await?.ok_or(ApiError::NotFound)?;
let user = get_current_user();
if article.author_id != user.id && !user.is_admin {
return Err(ApiError::Forbidden);
}
Ok(())
}
async fn check_delete_permission<C: sea_orm::ConnectionTrait + sea_orm::TransactionTrait>(
db: &C,
id: Uuid,
) -> Result<(), ApiError> {
let article = Entity::find_by_id(id).one(db).await?.ok_or(ApiError::NotFound)?;
let user = get_current_user();
if !user.is_admin {
return Err(ApiError::Forbidden);
}
Ok(())
}
Hooks vs CRUDOperations
| Feature | Hooks | CRUDOperations |
|---|---|---|
| Configuration | Attribute | Trait impl |
| Scope | Per-operation | All operations |
| State | Stateless functions | Can hold state |
| Testing | Function mocking | Trait mocking |
| Use when | Simple cases | Complex logic |
When to Use Hooks
- Simple validation
- Data transformation
- Logging/notifications
- Single operation customization
When to Use CRUDOperations
- Complex authorization logic
- Shared state across operations
- Database-dependent validation
- Multiple operations need same logic
Combining Both
You can use both hooks and CRUDOperations:
#[derive(EntityToModels)]
#[crudcrate(
generate_router,
operations = ArticleOperations,
create::one::pre = validate_article, // Runs before CRUDOperations::before_create
)]
pub struct Model { }
Execution order:
- Attribute hook (
create::one::pre) CRUDOperations::before_create- Database operation (or
create::one::bodyreplacement) - Attribute hook (
create::one::transform) CRUDOperations::after_create- Attribute hook (
create::one::post)
Hooks and Partial Success Mode
When using ?partial=true on batch endpoints, items are processed individually using single-item hooks (create::one::*, update::one::*, delete::one::*), not batch hooks (create::many::*, etc.). Each item commits independently with no shared transaction.
This means:
create::one::preruns once per item, notcreate::many::preonce for the batch- A failing item does not roll back previously succeeded items
- Transform hooks run per-item, receiving a single entity (not a
Vec)
See Security for more details on partial success behavior.
Next Steps
- Learn about Validation
- Configure Security
- Set up Performance Optimization