| Crates.io | crudcrate |
| lib.rs | crudcrate |
| version | 0.5.0 |
| created_at | 2025-02-04 14:21:18.664459+00 |
| updated_at | 2025-08-28 18:29:04.748016+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 | 237,760 |
Zero-boilerplate CRUD APIs for Sea-ORM and Axum.
crudcrate generates complete CRUD endpoints from your entities while working seamlessly alongside custom queries and handlers. No lock-in, just less repetitive code - use it where it helps, write custom logic where you need it.
cargo add crudcrate
Choose your database drivers to optimize binary size:
# Default: SQLite only
crudcrate = "0.4.1"
# Single database (smallest binary)
crudcrate = { version = "0.4.1", features = ["mysql"], default-features = false }
# Multiple databases (runtime flexibility)
crudcrate = { version = "0.4.1", features = ["mysql", "postgresql"] }
Available features: sqlite (default), mysql, postgresql
use crudcrate::EntityToModels;
use sea_orm::entity::prelude::*;
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, EntityToModels)]
#[sea_orm(table_name = "todos")]
#[crudcrate(generate_router)]
pub struct Model {
#[sea_orm(primary_key, auto_increment = false)]
#[crudcrate(primary_key, create_model = false, update_model = false)]
pub id: Uuid,
#[crudcrate(filterable, sortable)]
pub title: String,
#[crudcrate(filterable)]
pub completed: bool,
}
// Generates: Todo, TodoCreate, TodoUpdate structs + complete CRUD handlers + router() function
Use the generated router:
// During application startup - analyze indexes for optimization recommendations
let _ = crudcrate::analyse_all_registered_models(&db, false).await; // Compact output
// let _ = crudcrate::analyse_all_registered_models(&db, true).await; // With SQL commands
let app = Router::new()
.nest("/api/todos", router(&db)) // Generated router function
.with_state(db);
Generate API structs, CRUD operations, and handlers from Sea-ORM entities.
#[crudcrate(api_struct = "Task", description = "Task management")]
Control how fields behave in the generated API. See all field attributes
#[crudcrate(
primary_key, // Mark as primary key
filterable, // Enable filtering
sortable, // Enable sorting
fulltext, // Include in fulltext search
create_model = false, // Exclude from create operations
update_model = false, // Exclude from update operations
on_create = Uuid::new_v4(), // Auto-generate on create
on_update = Utc::now() // Auto-update on modification
)]
Multi-field search with database optimizations. See fulltext search architecture
#[crudcrate(fulltext)]
pub title: String,
#[crudcrate(fulltext)]
pub content: String,
GET /api/todos?filter={"q":"search term"}
React Admin compatible query parameters.
# Filtering
GET /api/todos?filter={"completed":false,"priority":"high"}
# Sorting
GET /api/todos?sort=created_at&order=DESC
# Pagination
GET /api/todos?page=0&per_page=20
Override default CRUD operations with custom logic. See custom function injection
#[crudcrate(fn_get_one = custom_get_one)]
pub struct Model { /* ... */ }
async fn custom_get_one(db: &DatabaseConnection, id: Uuid) -> Result<Todo, DbErr> {
// Custom implementation
}
The EntityToModels macro generates:
Todo with all public fieldsTodoCreate for POST requestsTodoUpdate with Option<Option<T>> patternrouter(db) with all endpoints configuredcrudcrate includes essential CRUD security (SQL injection prevention, input validation). For production applications, add:
[dependencies]
tower-http = { version = "0.6", features = ["cors", "trace"] }
axum-helmet = "0.1"
See tests/external_security_integration_test.rs for a complete example.
Sub-millisecond responses for typical operations:
See detailed performance characteristics
crudcrate automatically registers all models and analyzes your database for missing indexes at startup:
// Automatic registration - all models with EntityToModels are included
let _ = crudcrate::analyse_all_registered_models(&db, false).await; // Compact summary
let _ = crudcrate::analyse_all_registered_models(&db, true).await; // With SQL commands
Output:
crudcrate Index Analysis
═══════════════════════════
HIGH High Priority:
todos - Fulltext search on 2 columns without proper index
CREATE INDEX idx_todos_fulltext ON todos USING GIN (to_tsvector('english', title || ' ' || content));
MEDIUM Medium Priority:
todos - Field 'completed' is filterable but not indexed
CREATE INDEX idx_todos_completed ON todos (completed);
Copy and paste the SQL commands above to create missing indexes
crudcrate includes a comprehensive test suite with multi-database support to ensure compatibility across different database backends.
# Run all tests with SQLite (default, fastest)
cargo test
# Run specific test categories
cargo test --test crud_integration_test
cargo test --test fulltext_search_test
cargo test --test index_analysis_test
Test your application against multiple database backends to ensure compatibility:
Note: If you encounter test failures with PostgreSQL/MySQL due to schema conflicts, add
-- --test-threads=1to run tests sequentially.
# PostgreSQL testing (requires running PostgreSQL instance)
DATABASE_URL=postgres://postgres:pass@localhost/test_db cargo test
# MySQL testing (requires running MySQL instance)
DATABASE_URL=mysql://root:pass@127.0.0.1:3306/test_db cargo test -- --test-threads=1
# Test specific functionality on PostgreSQL
DATABASE_URL=postgres://postgres:pass@localhost/test_db cargo test --test fulltext_search_test
# Using Docker
docker run --name test-postgres \
-e POSTGRES_PASSWORD=pass \
-e POSTGRES_DB=test_db \
-p 5432:5432 -d postgres:16
# Run tests
DATABASE_URL=postgres://postgres:pass@localhost/test_db cargo test
# Cleanup
docker stop test-postgres && docker rm test-postgres
# Using Docker
docker run --name test-mysql \
-e MYSQL_ROOT_PASSWORD=pass \
-e MYSQL_DATABASE=test_db \
-p 3306:3306 -d mysql:8
# Wait for MySQL to fully initialize (important!)
echo "Waiting for MySQL to initialize..."
sleep 20
# Verify connection works
mysql -h 127.0.0.1 -P 3306 -u root -ppass -e "SELECT 1;" test_db
# Run tests (use single thread to avoid migration conflicts)
DATABASE_URL=mysql://root:pass@127.0.0.1:3306/test_db cargo test -- --test-threads=1
# Cleanup
docker stop test-mysql && docker rm test-mysql
Note: MySQL requires additional startup time and single-threaded testing to avoid migration conflicts. The
sleep 20ensures MySQL is fully ready, and--test-threads=1prevents race conditions with database migrations.
The test suite covers multiple areas:
Some tests demonstrate database-specific optimizations:
# Test PostgreSQL GIN index recommendations
DATABASE_URL=postgres://postgres:pass@localhost/test_db cargo test test_fulltext_columns_recommendations -- --nocapture
# Test SQLite fallback behavior
cargo test test_fulltext_search_sqlite_fallback
# Test MySQL fulltext capabilities
DATABASE_URL=mysql://root:pass@127.0.0.1:3306/test_db cargo test test_fulltext_search_with_different_data_types -- --test-threads=1
# SQLite benchmarks (default)
cargo bench --bench crud_benchmarks
# PostgreSQL benchmarks (requires Docker)
docker run --name benchmark-postgres -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=benchmark -p 5432:5432 -d postgres:16
BENCHMARK_DATABASE_URL=postgres://postgres:pass@localhost/benchmark cargo bench --bench crud_benchmarks
docker stop benchmark-postgres && docker rm benchmark-postgres
The EntityToModels macro analyzes your Sea-ORM entity and generates three main structures:
Option<Option<T>> pattern to distinguish between "don't update this field" (None) and "set this field to null" (Some(None))// Your entity
#[derive(EntityToModels)]
#[crudcrate(api_struct = "Todo")]
pub struct Model {
pub id: Uuid, // Excluded from Create model automatically
pub title: String, // Required in Create, optional in Update
pub completed: bool, // Required in Create, optional in Update
}
// Generated structures:
pub struct Todo { // API response struct
pub id: Uuid,
pub title: String,
pub completed: bool,
}
pub struct TodoCreate { // POST request body
pub title: String,
pub completed: bool,
// id excluded automatically
}
pub struct TodoUpdate { // PUT request body
pub title: Option<String>, // Some("new") = update, None = don't change
pub completed: Option<Option<bool>>, // Some(Some(true)) = set true, Some(None) = set null, None = don't change
}
Field attributes give you precise control over how each field behaves in different contexts:
#[crudcrate(
primary_key, // Marks this field as the primary identifier (only one per struct)
filterable, // Enables filtering: ?filter={"status":"active"}
sortable, // Enables sorting: ?sort=created_at&order=DESC
fulltext, // Includes in fulltext search: ?filter={"q":"search terms"}
)]
#[crudcrate(
create_model = false, // Excludes from TodoCreate struct (default: true)
update_model = false, // Excludes from TodoUpdate struct (default: true)
)]
#[crudcrate(
on_create = Uuid::new_v4(), // Expression to run on create operations
on_update = Utc::now(), // Expression to run on update operations
)]
#[crudcrate(
non_db_attr = true, // Field not in database (default: false)
default = vec![], // Default value for non-DB fields
// Requires #[sea_orm(ignore)] when using DeriveEntityModel
)]
#[crudcrate(
enum_field, // Mark field as enum for filtering (required for enum filtering)
enum_case_sensitive, // Enable case-sensitive enum matching (default: case-insensitive)
)]
Enum Field Requirements: For enum fields to work with filtering, you must explicitly mark them with enum_field:
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, EntityToModels)]
#[sea_orm(table_name = "products")]
pub struct Model {
#[sea_orm(primary_key)]
pub id: Uuid,
#[crudcrate(filterable, enum_field)] // ← Required for enum filtering
pub category: ProductCategory, // Sea-ORM enum type
}
This enables case-insensitive enum filtering where users can search for "elec" to find "Electronics".
Applied to the entire struct:
#[crudcrate(
api_struct = "TodoItem", // Override API struct name (default: table name in PascalCase)
name_singular = "todo", // Resource name singular (default: table name)
name_plural = "todos", // Resource name plural (default: singular + "s")
description = "Manages todos", // Resource description for OpenAPI docs
generate_router, // Auto-generate router function
fulltext_language = "simple", // PostgreSQL fulltext language (default: "english")
// Function injection - override default CRUD operations
fn_get_one = self::custom_get_one, // Custom get_one function
fn_get_all = self::custom_get_all, // Custom get_all function
fn_create = self::custom_create, // Custom create function
fn_update = self::custom_update, // Custom update function
fn_delete = self::custom_delete, // Custom delete function
fn_delete_many = self::custom_delete_many, // Custom batch delete function
)]
Fulltext search automatically optimizes based on your database backend:
PostgreSQL: Uses native tsvector and plainto_tsquery with GIN indexes for high-performance text search
-- Generated query for PostgreSQL (with GIN index support)
WHERE to_tsvector('english', title || ' ' || content) @@ plainto_tsquery('english', 'search terms')
-- Recommended index for optimal performance
CREATE INDEX idx_posts_fulltext ON posts USING GIN (to_tsvector('english', title || ' ' || content));
Language Configuration: Configure fulltext language per model for internationalization:
#[crudcrate(fulltext_language = "simple")] // Language-agnostic
#[crudcrate(fulltext_language = "spanish")] // Spanish text processing
#[crudcrate(fulltext_language = "french")] // French text processing
// Default: "english"
SQLite: Falls back to case-insensitive LIKE queries across all fulltext fields
-- Generated query for SQLite
WHERE (UPPER(title) LIKE UPPER('%search%') OR UPPER(content) LIKE UPPER('%terms%'))
MySQL: Uses MATCH AGAINST for fulltext indexes where available
crudcrate includes built-in protection against common vulnerabilities:
For production deployments, add these security layers:
use tower_http::{cors::CorsLayer, trace::TraceLayer};
use axum_helmet::Helmet;
let app = Router::new()
.nest("/api", your_crud_routes)
.layer(Helmet::default()) // Security headers
.layer(TraceLayer::new_for_http()) // Request logging
.layer(CorsLayer::permissive()); // CORS (configure for production)
crudcrate is optimized for high-throughput applications:
Benchmark your setup:
# Quick SQLite benchmark
cargo bench --bench crud_benchmarks -- --verbose
# Compare SQLite vs PostgreSQL performance
docker run --name benchmark-postgres -e POSTGRES_PASSWORD=pass -e POSTGRES_DB=benchmark -p 5432:5432 -d postgres:16
BENCHMARK_DATABASE_URL=postgres://postgres:pass@localhost/benchmark cargo bench --bench crud_benchmarks -- --verbose
docker stop benchmark-postgres && docker rm benchmark-postgres
Performance Differences:
tsvector and rankingcrudcrate follows React Admin's REST conventions out of the box:
// React Admin automatically understands these endpoints:
GET /api/todos // List with pagination
GET /api/todos?filter={"completed":false} // Filtered list
GET /api/todos/123 // Get one
POST /api/todos // Create
PUT /api/todos/123 // Update
DELETE /api/todos/123 // Delete
// Pagination parameters
GET /api/todos?page=0&per_page=25
// Sorting parameters
GET /api/todos?sort=created_at&order=DESC
// Complex filtering
GET /api/todos?filter={"title":"urgent","completed":false}
Override default CRUD behavior with your own implementations:
#[crudcrate(fn_get_one = custom_get_todo)]
pub struct Model { /* ... */ }
async fn custom_get_todo(db: &DatabaseConnection, id: Uuid) -> Result<Todo, DbErr> {
// Add custom logic: permissions, caching, audit trails, etc.
let todo = Entity::find_by_id(id)
.filter(Column::UserId.eq(current_user_id())) // Permission check
.one(db)
.await?;
// Log access for audit trail
audit::log_access("todo", id, current_user_id()).await;
todo.ok_or(DbErr::RecordNotFound("Todo not found"))
}
crudcrate works seamlessly with Sea-ORM's migration system:
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Todo::Table)
.col(ColumnDef::new(Todo::Id).uuid().not_null().primary_key())
.col(ColumnDef::new(Todo::Title).string().not_null())
.col(ColumnDef::new(Todo::Completed).boolean().not_null().default(false))
.to_owned(),
)
.await
}
}
Development of crudcrate and crudcrate-derive has occasionally been powered by the questionable wisdom of large language models. They have been consulted for prototyping, code suggestions, test generation, and the overuse of emojis in documentation. This has resulted in perhaps more verbose and less optimal implementations.
If you find this project useful and have a way to improve it, please help defeat the bots by contributing! 🤓
MIT License. See LICENSE for details.
Disclaimer: This software is provided "as is" without warranty of any kind. This library is in heavy development - use at your own risk. While crudcrate includes security measures for CRUD operations, users are responsible for implementing comprehensive security appropriate for their specific use case and environment.