Skip to main content
#[crud] is nestrs’s counterpart of @nestjsx/crud: one attribute on a controller struct generates the five REST verbs (or GraphQL operations), the backing service, and the state provider that carries the database pool. Every generated handler is wired through the standard nestrs pipeline — guards, interceptors, exception filters, and OpenAPI metadata all apply — and row-level authorization (authz-row-level) is enforced by the same deny-closed CrudService<T> the rest of the framework uses.
#[crud] requires the database-sqlx feature on nestrs, and #[crud] cannot be combined with #[routes(…)] — the macro generates the routes itself.

What gets generated

Given a controller named PostController, the macro emits: Handlers translate service results into HTTP: CrudError::NotFound becomes 404, validation failures become 422 with field detail, and SQL errors become sanitized 500s in production.

The moving parts you write

Three types participate, all yours to define:
  1. Entity — implements nestrs::Entity (table name, id access, row mapping). This is the storage layer type.
  2. Output DTO — what every handler returns. Derive Serialize, Deserialize, and NestDto.
  3. Create / Update DTOs — the accepted request bodies, also NestDto.

Defining the controller

The struct needs exactly one pool: Arc<sqlx::AnyPool> field and a #[controller(prefix = "...")] for the mount path:
Available options:

Wiring the pool and module

The macro cannot see your database URL, so the generated __PostCrudState provider defaults to no pool — you must override it with the real one before the app starts. DynamicModuleBuilder is the canonical way:
If the override is missing, every request fails with a 500 naming __PostCrudState — the default-constructed state has no pool. The override must happen before controllers are registered (the DynamicModuleBuilder sequence above does this correctly).

The list endpoint query contract

The list handler parses the query string with serde_qs, so bracketed keys survive. The contract mirrors @nestjsx/crud: Two evaluation tiers keep paging cheap:
  • Plain paging (page/per_page only): the window is pushed into SQL as LIMIT … OFFSET …, so the database materializes one page, not the whole table.
  • sort / filter / search present: the full set is fetched and evaluated in memory before slicing (the exact @nestjsx/crud contract), so expect these queries to cost proportionally to table size.

Examples

Invalid query strings return the framework’s standard error shapes: malformed bracket syntax → 400 with the parse error; validation failures (page=0, per_page=10000, bad sort syntax) → 422 with field-level detail.

Authorization

With the authz feature, generated routes compose with #[roles], guards, and policies like any hand-written route. With authz-row-level on, every generated handler flows through CrudService<T>’s deny-closed checks: no matching allow rule (or no Principal installed) means empty results on list/reads and 403/404 on writes — never a leaked row.
See the authorization guide for abilities, row predicates, and deny-closed semantics.

Transport variants

transport = "graphql" emits GraphQL mutations and queries instead of REST routes, following the same service and state wiring. The default transport = "http" (omit the option for REST).

Relation to hand-written controllers

#[crud] is a convenience for the standard 5-verb shape. When you need custom search joins, projections, or anything beyond the contract above, write the controller by hand with #[routes(...)] and call CrudService<T> — the generated service methods (list_query, get_one, create_one, update_one, delete_one) are the same ones you would call yourself.