#[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 namedPostController, 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:- Entity — implements
nestrs::Entity(table name, id access, row mapping). This is the storage layer type. - Output DTO — what every handler returns. Derive
Serialize,Deserialize, andNestDto. - Create / Update DTOs — the accepted request bodies, also
NestDto.
Defining the controller
The struct needs exactly onepool: Arc<sqlx::AnyPool> field and a #[controller(prefix = "...")] for the mount path:
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:
The list endpoint query contract
The list handler parses the query string withserde_qs, so bracketed keys survive. The contract mirrors @nestjsx/crud:
Two evaluation tiers keep paging cheap:
- Plain paging (
page/per_pageonly): the window is pushed into SQL asLIMIT … OFFSET …, so the database materializes one page, not the whole table. sort/filter/searchpresent: the full set is fetched and evaluated in memory before slicing (the exact@nestjsx/crudcontract), so expect these queries to cost proportionally to table size.
Examples
page=0, per_page=10000, bad sort syntax) → 422 with field-level detail.
Authorization
With theauthz 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.
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.