One macro to rule them all
Generate DTOs, repositories, mappers, and SQL from a single entity definition
Building a typical CRUD application requires writing the same boilerplate over and over: entity struct, create DTO, update DTO, response DTO, row struct, repository trait, SQL implementation, and 6+ From implementations.
That's 200+ lines of boilerplate for a single entity.
#[derive(Entity)]
#[entity(table = "users")]
pub struct User {
#[id]
pub id: Uuid,
#[field(create, update, response)]
pub name: String,
#[field(create, update, response)]
pub email: String,
#[field(skip)]
pub password_hash: String,
#[field(response)]
#[auto]
pub created_at: DateTime<Utc>,
}Done. The macro generates everything else.
[dependencies]
entity-derive = { version = "0.8", features = ["postgres", "api"] }| Feature | Default | What it does |
|---|---|---|
postgres |
β | Generate sqlx::PgPool-backed repository implementations |
events |
β | Generate {Entity}Event enum (Created / Updated / Deleted variants) |
commands |
β | CQRS command pattern: command structs + dispatcher (#[entity(commands)], #[command(...)]) |
hooks |
β | {Entity}Hooks trait with before/after lifecycle methods |
transactions |
β | {Entity}TransactionRepo adapter + transaction builder helpers (#[entity(transactions)]) |
aggregate_root |
β | New{Entity} constructor type and transactional save() (#[entity(aggregate_root)]) |
migrations |
β | Compile-time MIGRATION_UP / MIGRATION_DOWN SQL constants (#[entity(migrations)]) |
projections |
β | Projection structs and find_by_id_<projection> lookups (#[projection(...)]) |
clickhouse |
Generate ClickHouse-backed repositories (planned) | |
mongodb |
Generate MongoDB-backed repositories (planned) | |
streams |
{Entity}Subscriber using Postgres LISTEN/NOTIFY (pulls in events) |
|
api |
Generate HTTP handlers (axum) and utoipa OpenAPI schemas |
|
validate |
Wire up validator::Validate on generated DTOs |
|
tracing |
Wrap every generated async method in #[tracing::instrument] carrying entity + op span fields |
Default features cover the full entity-attribute surface so existing projects work without changes. For lean builds, opt out of what you don't need:
[dependencies]
# Just repositories β no events, hooks, commands, etc.
entity-derive = { version = "0.8", default-features = false, features = ["postgres"] }If you use an entity attribute whose feature is disabled (e.g. #[entity(commands)] without features = ["commands"]), the macro emits a compile_error! at the attribute pointing to the missing feature.
Enable extras alongside the defaults:
[dependencies]
entity-derive = { version = "0.8", features = ["postgres", "api", "tracing", "streams"] }
tracing = "0.1"
tracing-subscriber = "0.3"| Feature | Description |
|---|---|
| Zero Runtime Cost | All code generation at compile time |
| Type Safe | Change a field once, everything updates |
| Auto HTTP Handlers | api(handlers) generates CRUD endpoints + router |
OpenAPI Docs |
Auto-generated Swagger/OpenAPI documentation |
| Query Filtering | Type-safe #[filter], #[filter(like)], #[filter(range)] |
| Relations | #[belongs_to] and #[has_many] |
| Aggregate Roots | #[entity(aggregate_root)] with New{T} DTOs and transactional save |
| Transactions | Multi-entity atomic operations |
| Lifecycle Events | Created, Updated, Deleted events |
| Real-Time Streams | Postgres LISTEN/NOTIFY integration |
| Lifecycle Hook Traits | {Entity}Hooks trait emitted with before_create / after_update / etc.; invocation is currently manual at your service layer (tracking auto-invocation: #127) |
| CQRS Commands | Business-oriented command pattern |
| Soft Delete | deleted_at timestamp support |
| Structured Logging | Opt-in tracing feature wraps every generated async method in #[tracing::instrument] with entity + op fields |
| Topic | Languages |
|---|---|
| Getting Started | |
| Attributes | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Examples | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Features | |
| Filtering | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Relations | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Events | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Streams | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Hooks | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Commands | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Advanced | |
| Custom SQL | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Web Frameworks | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
| Best Practices | π¬π§ π·πΊ π°π· πͺπΈ π¨π³ |
#[entity(
table = "users", // Required: table name
schema = "public", // Optional: schema (default: omitted)
dialect = "postgres", // Optional: database dialect
aggregate_root, // Optional: New{T} DTOs + transactional save
soft_delete, // Optional: use deleted_at instead of DELETE
events, // Optional: generate lifecycle events
streams, // Optional: real-time Postgres NOTIFY
hooks, // Optional: before/after lifecycle hooks
commands, // Optional: CQRS command pattern
transactions, // Optional: multi-entity transaction support
api( // Optional: generate HTTP handlers + OpenAPI
tag = "Users",
handlers, // All CRUD, or handlers(get, list, create)
security = "bearer", // cookie, bearer, api_key, or none
title = "My API",
api_version = "1.0.0",
),
)]#[id] // Primary key (auto-generated UUID)
#[auto] // Auto-generated (timestamps)
#[field(create)] // Include in CreateRequest
#[field(update)] // Include in UpdateRequest
#[field(response)] // Include in Response
#[field(skip)] // Exclude from all DTOs
#[filter] // Exact match filter
#[filter(like)] // ILIKE pattern filter
#[filter(range)] // Range filter (from/to)
#[belongs_to(Entity)] // Foreign key relation
#[has_many(Entity)] // One-to-many relation
#[projection(Name: fields)] // Partial viewMark each participating entity with #[entity(table = "β¦", transactions)] and
drive a multi-entity transaction through Transaction::run. The closure
receives &mut TransactionContext; run commits on Ok and rolls back on
Err (or any panic) automatically:
use entity_core::transaction::Transaction;
Transaction::new(&pool)
.run(async |ctx| {
let user = ctx.users().create(create_user).await?;
ctx.orders().create(order_for(user.id)).await?;
Ok::<_, sqlx::Error>(user)
})
.await?;Need conditional commit/rollback inside the closure? Use
run_with_commit
β it takes TransactionContext by value so the closure can call
ctx.commit().await (or ctx.rollback().await) itself.
Opt-in with the tracing feature. Every generated async method
(create, find_by_id, update, delete, list, find_by_<field>,
projections, transaction adapters, stream subscribers) is wrapped in
#[tracing::instrument(skip_all, fields(entity, op), err(Debug))].
entity-derive = { version = "0.8", features = ["postgres", "tracing"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }With a subscriber initialized, a failed User::create surfaces as:
ERROR entity.User.create: error=database error: duplicate key value violates unique constraint
in entity.User.create with entity="User" op="create"
When the feature is off, generated code is byte-for-byte identical to a build without the attribute β zero runtime cost.
