Skip to content

Repository files navigation

entity-derive logo

entity-derive

One macro to rule them all

Generate DTOs, repositories, mappers, and SQL from a single entity definition

Crates.ioDocumentationCI Status

CoverageLicense: MITREUSE CompliantWikiDocumentation Site


The Problem

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.

The Solution

#[derive(Entity)]#[entity(table = "users")]pubstructUser{#[id]pubid:Uuid,#[field(create, update, response)]pubname:String,#[field(create, update, response)]pubemail:String,#[field(skip)]pubpassword_hash:String,#[field(response)]#[auto]pubcreated_at:DateTime<Utc>,}

Done. The macro generates everything else.


Installation

[dependencies]
entity-derive = { version = "0.22", features = ["postgres", "api"] }

Generated code reaches its own runtime through the entity-derive facade, so nothing else has to be added on its behalf. The crates you still declare are the ones your entity itself uses — the pool and column types, and serde when the DTOs are serialized:

sqlx = { version = "0.9", features = ["runtime-tokio", "postgres", "uuid", "chrono"] }
uuid = { version = "1", features = ["v4", "v7", "serde"] }
chrono = { version = "0.4", features = ["serde"] }
serde = { version = "1", features = ["derive"] }

Feature flags

FeatureDefaultWhat it does
postgresGenerate sqlx::PgPool-backed repository implementations
eventsGenerate {Entity}Event enum (Created / Updated / Deleted variants)
commandsCQRS 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_rootNew{Entity} constructor type and transactional save() (#[entity(aggregate_root)])
migrationsCompile-time MIGRATION_UP / MIGRATION_DOWN SQL constants (#[entity(migrations)])
projectionsProjection structs and find_by_id_<projection> lookups (#[projection(...)])
clickhouseNot implemented — selecting this dialect is a compile error (#23)
mongodbNot implemented — selecting this dialect is a compile error (#24)
streams{Entity}Subscriber using Postgres LISTEN/NOTIFY (pulls in events)
outboxTransactional-outbox enqueue in generated writes + OutboxDrainer runtime (pulls in events)
apiGenerate HTTP handlers (axum) and utoipa OpenAPI schemas
validateWire up validator::Validate on generated DTOs
gardeMaintained alternative: garde::Validate with translated #[validate(...)] rules (validate wins if both are on)
tracingWrap 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.22", 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.22", features = ["postgres", "api", "tracing", "streams"] }
tracing = "0.1"tracing-subscriber = "0.3"

Features

FeatureDescription
Zero Runtime CostAll code generation at compile time
Type SafeChange a field once, everything updates
Auto HTTP Handlersapi(handlers) generates CRUD endpoints + router
OpenAPI DocsAuto-generated Swagger/OpenAPI documentation
Query FilteringType-safe #[filter], #[filter(like)], #[filter(range)], #[filter(search)]
Sorting & Keyset#[sort] whitelisted ORDER BY + list_after cursor pagination
Bulk Operationsfind_by_ids, atomic create_many, soft-delete-aware delete_many
PATCH SemanticsDynamic UPDATE SET; double-Option distinguishes "leave" from "set NULL"
Optimistic Locking#[version] — guarded, auto-incremented version column
Typed Constraint Errorstyped_constraints — violations resolved to ConstraintError with field info
OpenAPI Overrides#[schema(...)] forwarded verbatim to the generated create/response DTOs and joined views
Embedded Value Objects#[embed(prefix, fields(...))] — structs flattened to prefixed columns
Relations#[belongs_to], #[has_many] and many-to-many via through = "junction"
Ownership Scoping#[owner] generates find_by_id_scoped / list_by_owner / update_scoped / delete_scoped
Participant Scopes#[scope(name: col_a | col_b)] generates list_{name} over an OR group, optionally narrowed by within
Upsertupsert(conflict = "…") generates INSERT ... ON CONFLICT DO UPDATE / DO NOTHING
Aggregate Roots#[entity(aggregate_root)] with New{T} DTOs and transactional save
TransactionsMulti-entity atomic operations
Lifecycle EventsCreated, Updated, Deleted events
Real-Time StreamsPostgres LISTEN/NOTIFY integration
Transactional Outboxevents(outbox) — durable at-least-once event delivery with retry/backoff
Lifecycle Hooks{Entity}Hooks trait plus {Entity}Repo<H>, a repository that runs the hooks around every mutation
CQRS CommandsBusiness-oriented command pattern; sets(...) turns one into a domain operation writing named columns
Soft Deletedeleted_at timestamp support
Structured LoggingOpt-in tracing feature wraps every generated async method in #[tracing::instrument] with entity + op fields

Documentation

TopicLanguages
Getting Started
Attributes🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Examples🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Features
Filtering🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Relations🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Events🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Streams🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Hooks🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Commands🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Advanced
Custom SQL🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Web Frameworks🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳
Best Practices🇬🇧🇷🇺🇰🇷🇪🇸🇨🇳

Quick Reference

Entity Attributes

#[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 migrations(// Optional: extra DDL alongside the table touch_updated_at,// updated_at trigger (shared function) audit,// JSONB audit-log table + trigger extensions = "pg_trgm",// CREATE EXTENSION statements), typed_constraints,// Optional: constraint violations as typed errors upsert(// Optional: INSERT ... ON CONFLICT method conflict = "email",// #[column(unique)] field(s) or unique_index columns action = "update",// "update" (default) or "nothing"), events,// Optional: generate lifecycle events events(outbox),// Optional: + durable transactional outbox 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 guard = "RequireAuth",// FromRequestParts extractor enforced in handlers guard(list = "none"),// per-op override: create/get/update/delete/list/commands title = "My API", api_version = "1.0.0",),)]

Field Attributes

#[id]// Primary key (auto-generated UUID)#[auto]// Auto-generated: skipped by INSERT, gets a DB default in migrations#[owner]// Ownership column: adds *_scoped methods#[version]// Optimistic locking: guarded, auto-bumped#[embed(prefix, fields(...))]// Flatten a value object to prefixed columns#[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 substring filter; pass the bare value, wildcards in it are escaped#[filter(range)]// Range filter (from/to)#[filter(search)]// Trigram substring search (pg_trgm)#[sort]// Whitelisted dynamic ORDER BY#[belongs_to(Entity)]// Foreign key relation#[has_many(Entity)]// One-to-many relation#[has_many(E, through = "t")]// Many-to-many via junction table#[projection(Name: fields)]// Partial view#[scope(name: col_a | col_b)]// list_{name} over an OR group of columns#[schema(value_type = T)]// OpenAPI type override, forwarded to the generated structs

Transactional Outbox

LISTEN/NOTIFY (streams) is fire-and-forget: events are lost if no subscriber is listening. events(outbox) makes delivery durable — every generated write inserts the serialized event into the entity_outbox table in the same transaction as the DML, and a drainer delivers rows with retry and exponential backoff:

#[derive(Entity,Serialize,Deserialize)]#[entity(table = "orders", events(outbox), migrations)]pubstructOrder{/* ... */}
sqlx::query(Order::MIGRATION_OUTBOX).execute(&pool).await?;structNotifier;#[async_trait::async_trait]impl entity_core::outbox::OutboxHandlerforNotifier{typeError = anyhow::Error;asyncfnhandle(&self,row:&OutboxRow) -> Result<(),Self::Error>{deliver(&row.entity,&row.payload).await}}
entity_core::outbox::OutboxDrainer::new(pool,Notifier).run().await;

Rows are claimed with FOR UPDATE SKIP LOCKED (multiple drainers cooperate), retried with exponential backoff and parked after max_attempts for manual inspection. Delivery is at-least-once — handlers must be idempotent. Composes with streams: NOTIFY wakes subscribers instantly, the outbox guarantees nothing is lost. Requires the outbox feature and serde_json in your crate.

Lifecycle Hooks

#[entity(hooks)] emits the {Entity}Hooks trait and {Entity}Repo<H>, a repository that owns a pool and a hooks implementation and runs the hooks around every mutation:

#[derive(Entity)]#[entity(table = "users", hooks)]pubstructUser{/* ... */}let repo = UserRepo::new(pool,Audit);let user = repo.create(dto).await?;// before_create → INSERT → after_createlet found = repo.find_by_id(id).await?;// reads carry no hooks

A failing before_* aborts before anything is written; after_delete and after_restore run only when a row was actually affected. The hook error only has to convert into the repository error, so hooks may keep their own error type. The bare pool keeps working exactly as before — the wrapper is opt-in.

Domain Operations

Columns that must change only through a named operation cannot be #[field(update)] — that would put them in the public patch DTO and in the upsert SET list. Declare the operation instead:

#[derive(Entity)]#[entity(table = "citizens", commands)]#[command(VerifyPassport, payload(passport_provider), sets( passport_verified = "true", passport_verified_at = "NOW()"))]pubstructCitizen{/* ... */}let verified = pool.verify_passport(VerifyPassportCitizen{
id,passport_provider:Some("gov".into()),}).await?;

One UPDATE writes the fixed expressions plus the payload columns and nothing else. The expressions land in the statement verbatim, like #[column(default = "...")]; the column names are checked against the entity at compile time.

With transactions on, the same operation is available on the adapter, so it can land together with other writes — there it returns Ok(None) for a missing row, like the adapter's other methods:

letmut tx = pool.begin().await?;let verified = CitizenTransactionRepo::new(&mut tx).verify_passport(VerifyPassportCitizen{ id,passport_provider:Some("gov".into())}).await?;
tx.commit().await?;

Participant Scopes

"Rows where this principal takes part in any role" is an OR over several columns holding the same kind of value. Declare it once:

#[derive(Entity)]#[entity(table = "disputes")]#[scope(involving: requester_id | subject_id)]#[scope(handled: requester_id | subject_id, within = parcel_id)]pubstructDispute{/* ... */}let mine = pool.list_involving(user_id,20,0).await?;let here = pool.list_handled(parcel_id, user_id,20,0).await?;

The value is bound once and matched against every declared column; within narrows the group to one parent first. Columns are checked at compile time: an unknown name or a group whose columns disagree on their type is an error at the declaration. Soft-delete aware, ordered by id descending.

Ownership Scoping

Mark the column carrying the owning principal's id with #[owner] and the repository gains row-level scoped methods — "only this user's rows" without hand-written predicates:

#[derive(Entity)]#[entity(table = "orders")]pubstructOrder{#[id]pubid:Uuid,#[owner]pubuser_id:Uuid,#[field(create, update, response)]pubnote:String,}let mine:Vec<Order> = pool.list_by_owner(user_id,20,0).await?;let order = pool.find_by_id_scoped(id, user_id).await?;// None if not theirslet updated = pool.update_scoped(id, user_id, patch).await?;// None if not theirslet removed = pool.delete_scoped(id, user_id).await?;// false if not theirs

Scoped reads and writes never reveal whether a row exists for another owner, and all of them respect soft_delete.

Handler Guards

security = "..." only documents authentication in the OpenAPI spec. To actually enforce it, pass a guard — any type implementing axum's FromRequestParts. It is injected as a leading argument of every generated handler, so a failed extraction rejects the request before the handler body runs:

pubstructRequireAuth;impl<S:Send + Sync>FromRequestParts<S>forRequireAuth{typeRejection = StatusCode;asyncfnfrom_request_parts(parts:&mutParts, _:&S) -> Result<Self,Self::Rejection>{
parts.headers.contains_key("authorization").then_some(Self).ok_or(StatusCode::UNAUTHORIZED)}}#[derive(Entity)]#[entity(table = "users", api(tag = "Users", handlers, guard = "RequireAuth", guard(list = "none")))]pubstructUser{/* ... */}

Per-operation overrides accept create, get, update, delete, list and commands; the literal "none" disables the guard for that operation. Commands listed in public = [...] never receive a guard.

Embedded Value Objects

Keep money-style value objects as real structs on the entity while storing them as flat scalar columns — no JSONB detour:

pubstructMoney{pubamount_cents:i64,pubcurrency:String}#[derive(Entity)]#[entity(table = "products", migrations)]pubstructProduct{#[id]pubid:Uuid,#[field(create, update, response)]#[embed(prefix = "price_", fields(amount_cents:i64, currency:String))]pubprice:Money,}

DDL, Row struct, CRUD SQL and dynamic updates operate on price_amount_cents / price_currency; DTOs and the entity carry Money itself. The declared shape is destructured against the real struct at compile time — a renamed, retyped, missing or extra field fails the build. Option<Money> parents are not supported yet.

Typed Constraint Errors

The macro knows every constraint it creates. With typed_constraints, write methods resolve violated constraint names and surface entity_core::ConstraintError { kind, constraint, field } instead of a raw driver error — no string-matching constraint names in handlers:

#[entity(table = "users", typed_constraints, error = "AppError")]pubstructUser{#[id]pubid:Uuid,#[field(create, response)]#[column(unique)]pubemail:String,}match pool.create(dto).await{Err(AppError::Constraint(v))if v.field == Some("email") => conflict_409(),
other => other?,}

Covers unique columns, belongs_to foreign keys, column checks and unique_index names. Constraints the macro cannot infer — foreign keys over natural keys, custom-named CHECKs, indexes from hand-written migrations — are declared explicitly and take precedence over derived entries with the same name:

#[entity( table = "orders", typed_constraints, constraint(name = "orders_currency_fkey", kind = "foreign_key", field = "currency"), constraint(name = "orders_window_check", kind = "check"),)]

The custom error type must implement From<ConstraintError>; without the flag behavior is unchanged.

Trigram Search

#[filter(search)] on a text column gives the Query struct a fuzzy substring filter (col ILIKE '%' || $n || '%'), while migrations emit the matching gin_trgm_ops index and add pg_trgm to MIGRATION_EXTENSIONS automatically. Unlike #[filter(like)], the value is bound as given: a % inside it acts as a wildcard rather than matching literally.

#[field(create, update, response)]#[filter(search)]pub title:String,let hits = pool.query(ArticleQuery{title:Some("rust".into()), ..Default::default()}).await?;

Optimistic Locking

Mark an integer column with #[version] and concurrent updates stop silently overwriting each other. The Update DTO gains a required expected_version; the generated UPDATE bumps the column and only applies when the stored version still matches — a stale write fails with a conflict error instead of clobbering newer data:

#[derive(Entity)]#[entity(table = "orders", migrations)]pubstructOrder{#[id]pubid:Uuid,#[field(create, update, response)]pubnote:String,#[version]#[field(response)]#[auto]pubversion:i32,}let patch = UpdateOrderRequest{note:Some("v2".into()),expected_version: order.version};let updated = pool.update(order.id, patch).await?;// version = version + 1

DDL gets INTEGER NOT NULL DEFAULT 0 automatically. Works in plain, scoped and transactional updates.

Migration Triggers & Extensions

migrations(...) options emit the DDL production tables actually carry:

#[entity(table = "articles", migrations(touch_updated_at, audit, extensions = "pg_trgm"))]pubstructArticle{/* ... */}for ddl inArticle::MIGRATION_EXTENSIONS{ sqlx::query(ddl).execute(&pool).await?;}
sqlx::query(Article::MIGRATION_UP).execute(&pool).await?;for ddl inArticle::MIGRATION_TRIGGERS{ sqlx::query(ddl).execute(&pool).await?;}
  • touch_updated_at — shared entity_touch_updated_at() function + per-table BEFORE UPDATE trigger keeping updated_at fresh DB-side (requires an updated_at field, checked at compile time)
  • auditentity_audit_log table + trigger recording to_jsonb(OLD/NEW) diffs for INSERT/UPDATE/DELETE
  • extensions = "..." — idempotent CREATE EXTENSION statements

PATCH Semantics

Update DTOs are true partial patches. Fields absent from the payload are left untouched — the generated UPDATE only includes columns actually present. Nullable columns use double-Option, so "leave unchanged" and "set NULL" are finally distinguishable:

// {} → nothing changes (fetch-and-return)// {"nickname": null} → nickname = NULL// {"nickname": "neo"} → nickname = 'neo'let patch:UpdateProfileRequest = serde_json::from_str(body)?;let profile = pool.update(id, patch).await?;

In Rust code: None = leave, Some(None) = SET NULL, Some(Some(v)) = SET v. Chainable setters say the same thing without the nesting — set_{field} for every updatable column, clear_{field} for the nullable ones, expecting_version where #[version] is declared:

let patch = UpdateProfileRequest::default().set_display_name("Neo".into())// SET display_name = 'Neo'.clear_nickname();// SET nickname = NULLlet profile = pool.update(id, patch).await?;

Struct-literal construction keeps working; the setters are additive.

Bulk Operations

Every repository ships batch primitives — one round-trip reads and atomic writes:

let posts:Vec<Post> = pool.find_by_ids(ids).await?;// WHERE id = ANY($1)let created:Vec<Post> = pool.create_many(dtos).await?;// one transactionlet removed:u64 = pool.delete_many(stale_ids).await?;// soft-delete aware

create_many rolls the whole batch back if any row fails; delete_many returns the number of rows actually affected. With events delivery (streams or outbox) per-row events are emitted inside the same transaction.

Sorting & Keyset Pagination

Mark sortable columns with #[sort] — the Query struct gains a whitelisted sort selector ({Entity}SortField, one Asc/Desc variant per column, JSON-friendly), so user input can never inject SQL. Every repository also gets list_after keyset pagination that stays fast on deep pages, unlike OFFSET:

let query = PostQuery{sort:Some(PostSortField::ViewsDesc),limit:Some(20),
..Default::default()};let top:Vec<Post> = pool.query(query).await?;let page:Vec<Post> = pool.list_after(None,20).await?;let next:Vec<Post> = pool.list_after(page.last().map(|p| p.id),20).await?;

With the default UUIDv7 ids, the id-ordered keyset walk is chronologically stable.

Many-to-Many Relations

Declare the junction table with through and the repository gains a JOIN-backed lookup plus link management, while migrations emits the junction DDL (composite primary key, cascading foreign keys):

#[derive(Entity)]#[entity(table = "teams", migrations)]#[has_many(User, through = "team_members")]pubstructTeam{/* ... */}for ddl inTeam::MIGRATION_JUNCTIONS{
sqlx::query(ddl).execute(&pool).await?;}
pool.add_user(team_id, user_id).await?;// idempotent linklet members:Vec<User> = pool.find_users(team_id).await?;let linked = pool.has_user(team_id, user_id).await?;let removed = pool.remove_user(team_id, user_id).await?;

Postgres Enums

Derive ValueObject on a status-style enum and reference it from entities with #[column(pg_enum = "...")]:

#[derive(ValueObject,Debug,Clone,Serialize,Deserialize)]#[value_object(pg_type = "order_status", sqlx)]pubenumOrderStatus{Pending,Shipped,Delivered}#[derive(Entity)]#[entity(table = "orders", migrations)]pubstructOrder{#[id]pubid:Uuid,#[field(create, update, response)]#[column(pg_enum = "order_status")]pubstatus:OrderStatus,}for ddl inOrder::MIGRATION_TYPES{
sqlx::query(ddl).execute(&pool).await?;}
sqlx::query(Order::MIGRATION_UP).execute(&pool).await?;
  • ValueObject generates PG_TYPE and idempotent PG_CREATE_TYPE constants; the opt-in sqlx flag additionally emits sqlx::Type / Encode / Decode impls so the enum binds and decodes without hand-written glue (omit it if you already derive sqlx::Type yourself).
  • #[column(pg_enum = "...")] sets the DDL column type and registers the enum's DDL in {Entity}::MIGRATION_TYPES — run those before MIGRATION_UP.
  • The declared name is checked against the enum's pg_type at compile time; a typo fails the build.

Upsert

Declare a conflict target with a uniqueness guarantee (#[id], #[column(unique)] or unique_index(...)) and the repository gains an upsert method backed by INSERT ... ON CONFLICT:

#[derive(Entity)]#[entity(table = "users", upsert(conflict = "email"))]pubstructUser{#[id]pubid:Uuid,#[field(create, response)]#[column(unique)]pubemail:String,#[field(create, update, response)]pubname:String,}let user = pool.upsert(CreateUserRequest{ email, name }).await?;

action = "update" (default) overwrites all non-conflict columns with the incoming values (DO UPDATE SET col = EXCLUDED.col) and returns the persisted row. action = "nothing" keeps the existing row (DO NOTHING) and returns Option<Entity>None when a conflicting row already existed. Requires returning = "full" (the default). With streams enabled, upsert publishes a Created notification for every row it returns. With transactions, the {Entity}TransactionRepo adapter gains the same upsert so it can share atomicity with adjacent statements (release-then-upsert login flows and the like).

Transactions

Mark 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.

Validation Backends

#[validate(...)] constraints on entity fields translate to the chosen backend. validate derives validator::Validate (unchanged). The new garde feature derives garde::Validate instead, translating length/range/email/url/pattern rules (Option-wrapped DTO fields validate the inner value; unconstrained fields get garde(skip)). validator_derive depends on the unmaintained proc-macro-error2 (RUSTSEC-2026-0173) — garde avoids it. When both features are enabled, validate takes precedence.

Tracing

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.22", 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.


Code Coverage

Coverage Sunburst


Project

DocumentPurpose
CONTRIBUTING.mdBranch, commit and PR conventions; local checks; the live Postgres suite
SUPPORT.mdWhere to ask what, and what a good issue contains
SECURITY.mdPrivate vulnerability reporting, supported versions, what is in scope
CODE_OF_CONDUCT.mdExpected behaviour in project spaces
RELEASE.mdHow a release is cut and how versions are decided
STABILITY.mdWhat the generated API guarantees across versions

About

Derive macro that generates DTOs, repositories, SQL queries, REST handlers, and OpenAPI docs from a single Rust struct definition

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

5 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Used by

Contributors

Languages