Skip to content

Best Practices en

RAprogramm edited this page Jan 7, 2026 · 2 revisions

Best Practices

Guidelines for using entity-derive effectively in production.

Entity Design

Keep Entities Focused

One entity per database table. Don't try to model complex relationships in a single entity.

// Good: Separate entities
#[derive(Entity)]
#[entity(table = "users")]
pub struct User {
    #[id]
    pub id: Uuid,
    #[field(create, update, response)]
    pub name: String,
}

#[derive(Entity)]
#[entity(table = "posts")]
pub struct Post {
    #[id]
    pub id: Uuid,
    #[field(create, response)]
    pub author_id: Uuid,  // Reference, not embed
    #[field(create, update, response)]
    pub title: String,
}

// Bad: Trying to embed relationships
pub struct User {
    pub id: Uuid,
    pub posts: Vec<Post>,  // Don't do this
}

Use Meaningful Field Attributes

Be explicit about each field's purpose:

// Good: Clear intent
#[field(create, response)]      // Set once, always visible
pub email: String,

#[field(update, response)]      // Can change, always visible
pub display_name: Option<String>,

#[field(response)]              // Read-only, computed/managed elsewhere
pub post_count: i64,

#[field(skip)]                  // Never exposed
pub password_hash: String,

// Bad: Everything everywhere
#[field(create, update, response)]  // Is this really needed for all?
pub internal_id: String,

Prefer Option for Nullable Fields

Match your database schema:

// Database: email VARCHAR NOT NULL
#[field(create, update, response)]
pub email: String,

// Database: bio TEXT NULL
#[field(update, response)]
pub bio: Option<String>,

Security

Always Use #[field(skip)] for Sensitive Data

// Passwords
#[field(skip)]
pub password_hash: String,

// API keys
#[field(skip)]
pub api_key: String,

// Internal tokens
#[field(skip)]
pub refresh_token: Option<String>,

// PII that shouldn't be in responses
#[field(skip)]
pub ssn: String,

// Internal audit data
#[field(skip)]
pub created_by_ip: String,

Separate Internal and External Entities

For admin-only data, consider separate entities:

// Public entity
#[derive(Entity)]
#[entity(table = "users")]
pub struct User {
    #[id]
    pub id: Uuid,
    #[field(create, update, response)]
    pub name: String,
    #[field(skip)]
    pub admin_notes: Option<String>,
}

// Admin-only entity (same table, different view)
#[derive(Entity)]
#[entity(table = "users", sql = "trait")]
pub struct AdminUser {
    #[id]
    pub id: Uuid,
    #[field(response)]
    pub name: String,
    #[field(update, response)]  // Now visible and editable
    pub admin_notes: Option<String>,
    #[field(response)]
    pub last_login_ip: Option<String>,
}

Performance

Use sql = "trait" for Complex Queries

Don't fight the generated SQL. If you need joins or complex logic, implement it yourself:

// Simple CRUD - use full generation
#[entity(table = "categories", sql = "full")]

// Complex queries needed - implement yourself
#[entity(table = "posts", sql = "trait")]

Batch Operations

For bulk inserts, implement custom methods:

#[entity(table = "events", sql = "trait")]
pub struct Event { /* ... */ }

pub trait EventBatchRepository {
    async fn create_batch(&self, events: Vec<CreateEventRequest>) -> Result<(), sqlx::Error>;
}

#[async_trait]
impl EventBatchRepository for PgPool {
    async fn create_batch(&self, events: Vec<CreateEventRequest>) -> Result<(), sqlx::Error> {
        let mut tx = self.begin().await?;

        for event in events {
            let entity = Event::from(event);
            let insertable = InsertableEvent::from(&entity);
            // Insert within transaction
        }

        tx.commit().await?;
        Ok(())
    }
}

Avoid N+1 Queries

Use joins instead of loading related entities one by one:

// Bad: N+1 queries
let posts = pool.list(100, 0).await?;
for post in &posts {
    let author = pool.find_user_by_id(post.author_id).await?;  // N queries!
}

// Good: Single query with join
let posts_with_authors = pool.list_with_authors(100, 0).await?;  // 1 query

Testing

Use Separate Test Database

#[cfg(test)]
mod tests {
    use sqlx::PgPool;

    async fn setup_test_db() -> PgPool {
        let url = std::env::var("TEST_DATABASE_URL")
            .expect("TEST_DATABASE_URL must be set");

        let pool = PgPool::connect(&url).await.unwrap();

        // Run migrations
        sqlx::migrate!("./migrations")
            .run(&pool)
            .await
            .unwrap();

        pool
    }

    #[tokio::test]
    async fn test_create_user() {
        let pool = setup_test_db().await;

        let request = CreateUserRequest {
            username: "test_user".into(),
            email: "test@example.com".into(),
        };

        let user = pool.create(request).await.unwrap();
        assert_eq!(user.username, "test_user");
    }
}

Test DTOs Separately

#[test]
fn test_user_response_excludes_password() {
    let user = User {
        id: Uuid::new_v4(),
        username: "test".into(),
        email: "test@example.com".into(),
        password_hash: "secret_hash".into(),
        created_at: Utc::now(),
    };

    let response = UserResponse::from(&user);

    // password_hash is not in UserResponse
    assert_eq!(response.username, "test");
    // No way to access password_hash through response
}

#[test]
fn test_update_request_is_partial() {
    let update = UpdateUserRequest {
        username: Some("new_name".into()),
        email: None,  // Not updating email
    };

    assert!(update.username.is_some());
    assert!(update.email.is_none());
}

Project Organization

Recommended Structure

src/
├── entities/           # Entity definitions
│   ├── mod.rs
│   ├── user.rs
│   ├── post.rs
│   └── comment.rs
├── repositories/       # Custom repository extensions
│   ├── mod.rs
│   └── post_search.rs
├── handlers/           # HTTP handlers
│   ├── mod.rs
│   ├── users.rs
│   └── posts.rs
├── services/           # Business logic
│   ├── mod.rs
│   └── auth.rs
└── main.rs

Re-export Generated Types

// src/entities/mod.rs
mod user;
mod post;

pub use user::*;
pub use post::*;

Group Related Entities

// src/entities/auth/mod.rs
mod user;
mod session;
mod api_key;

pub use user::*;
pub use session::*;
pub use api_key::*;

Common Mistakes

1. Forgetting #[field(skip)] on Sensitive Fields

// Wrong: password_hash will be in Response!
pub struct User {
    pub password_hash: String,
}

// Right
#[field(skip)]
pub password_hash: String,

2. Using sql = "full" When You Need Joins

If you need related data, use sql = "trait" and implement yourself.

3. Not Handling Optional Updates

Remember: UpdateRequest fields are Option<T>. Check before applying:

// Generated UpdateUserRequest has Option<String> for name
// Your update logic should handle None (no change) vs Some (change)

4. Duplicating Business Logic

Put validation and business rules in a service layer, not in handlers:

// Good: Service layer
impl UserService {
    pub async fn create_user(&self, request: CreateUserRequest) -> Result<User, AppError> {
        self.validate_email(&request.email)?;
        self.check_username_available(&request.username).await?;
        self.pool.create(request).await.map_err(Into::into)
    }
}

// Bad: Logic scattered in handlers
pub async fn create_user(pool: State<PgPool>, request: Json<CreateUserRequest>) -> ... {
    // Validation here
    // Business rules here
    // Repository call here
    // All mixed together
}

Checklist

Before deploying:

  • All sensitive fields have #[field(skip)]
  • DTOs match API contract expectations
  • Complex queries use sql = "trait"
  • Integration tests cover repository methods
  • Error handling is consistent
  • Pagination is implemented for list endpoints
  • Database indexes exist for query patterns

Clone this wiki locally