CRUD Workflow
This guide explains the Vona CRUD generator workflow in the Cabloy monorepo.
Why this page matters
CRUD is one of the clearest places where Cabloy’s CLI-first philosophy pays off.
Instead of creating controller, service, model, entity, DTO, metadata, locale, and test files by hand, Vona already provides generators that create the initial backend thread.
NOTE
The fullstack tutorial series intentionally uses a standalone demo-student sandbox so readers can experiment without colliding with the repo's real suite-owned a-training/training-student implementation. This guide, by contrast, uses the current repo implementation as its compact specimen.
Generate a CRUD skeleton
Example: generate a CRUD workflow for student in module training-student.
npm run vona :tools:crud student -- --module=training-studentA lighter variant also exists:
npm run vona :tools:crudBasic student -- --module=training-studentThis is important because the repo already encodes both the full CRUD thread and a lighter CRUD-basic workflow in the CLI surface.
Generated structure
The generator creates a connected set of files, typically including:
- controller
- service
- model
- entity
- create/update DTOs
- meta version and index files
- locale files
- tests
This is exactly why this generator should be the default starting point. It gives a consistent starting shape across the backend thread.
Generated authorization default
The standard generated CRUD actions (create, select, view, update, and delete) each use @Passport.systemAdmin(). This is an explicit administrative whitelist for the generated Admin resource API, matching the generated SSR menu's systemAdmin visibility policy.
The guard is attached to individual actions rather than the controller class. When you add a custom action later, no local Passport guard intentionally means the global authenticated-and-activated baseline applies; it does not mean the action is public. Use @Passport.public() to intentionally allow anonymous access, or add a role/domain guard when an action needs stronger restriction.
SSR menu roles control navigation disclosure only. They never authorize a controller action, API, or resource; keep the action guard as the server-side authorization boundary. See Controller AOP Guide and Menu Guide.
Generated mutation response default
Generated resource update and delete actions are command-style actions. Their controllers use Promise<void>, await the service call without forwarding its result, and declare @Api.body(z.null()) so OpenAPI and generated SDKs describe the normal HTTP 200 success wrapper accurately as data: null.
This controller-facing default does not require the service or model mutation method to return void. Those lower layers may retain mutation data for internal orchestration. See Controller Guide when a mutation deliberately needs a consumer-facing response payload.
A practical generated-output checklist usually includes:
- controller
- service
- model
- entity
- create/update DTOs
meta.version- locale assets
- test file
- package-version update for the next schema step
That checklist is useful because it makes the generated thread easier to inspect after the CLI run instead of treating CRUD generation as a black box.
The generated backend thread
The CRUD generator is not a shortcut around the architecture. It instantiates the same backend contract loop documented elsewhere.
A practical thread is:
- controller exposes the HTTP contract
- service owns orchestration
- model owns persistence behavior
- entity defines the field/data contract
- DTOs define operation-specific request/response contracts
- meta.version handles schema lifecycle
- tests verify the resulting contract through action execution
Read this guide together with:
Instance-scoped CRUD
Generated controller, service, and model flows retain Vona's default active-instance scope. In the current tenancy model, a tenant corresponds to an instance, so ordinary resource CRUD should not accept caller-controlled iid or tenant selection.
When a normal scoped view, update, or delete target is not found, preserve its absent/not-found semantics. Do not run an unscoped raw-table probe merely to discover whether another instance owns the same identifier and convert that result into 403.
If a future requirement introduces multiple merchants inside one instance, model that as an explicit additional business boundary with its own ownership, authorization, relations, indexes, and tests. It does not replace the existing instance scope.
For the runtime tenancy boundary, see Multi-Instance and Instance Resolution. For model-level behavior and exceptional raw/builder usage, see Model Guide.
Recommended workflow
- run the CRUD generator
- inspect the generated files
- refine entity, DTO, model, service, and controller behavior for the real business case
- verify routes, model behavior, migration behavior, and tests
A practical expectation is that the generated test should already help verify the full contract thread rather than only file existence. In other words, generation should leave you with something that can immediately participate in CRUD-oriented action testing, migration verification, and later OpenAPI/frontend contract refinement.
This is the preferred path because it preserves framework conventions first, then applies domain-specific refinement second.
When to keep generated defaults vs refine them manually
A practical rule is:
- keep generated defaults when the backend thread already matches the business shape
- refine the generated code when response contracts, DTO behavior, controller metadata, model behavior, or test flow need stronger domain-specific semantics
- avoid replacing the generated thread wholesale unless the framework shape truly does not fit the use case
Aggregate detail scaffolding
When the business shape is not a standalone CRUD resource but a master resource that owns a nested detail collection, use the master-detail generator instead of hand-wiring the relation and DTO thread.
Example:
npm run vona :tools:masterDetail student -- --module=training-student --detailModule=training-record --detailResourceName=record --relationName=trainingRecords --fk=studentId --detailMode=aggregateThis command is intended to scaffold the aggregate-detail thread that the training-student / training-record specimen demonstrates:
- master model
hasManyrelation - master service
includelifecycle - master-side nested detail DTOs
- built-in
basic-detailsbulk/row actions - detail FK persistence and index wiring
For --module and --detailModule, use canonical Vona module relative names such as training-student and training-record, not package names such as vona-module-training-student.
Two supported detail-module modes
Use --detailMode=aggregate when the detail module should remain entity/model/meta-only and should not expose its own standalone controller/service resource surface.
Use --detailMode=standalone when the detail module should participate in the master aggregate but also keep its own standalone resource surface.
A practical rule is:
- choose
aggregatewhen the detail is owned and managed primarily inside the master workflow - choose
standalonewhen the detail also needs independent resource entry/use cases
The generator should be the default starting point for this pattern, and manual refinement should come afterward.
Relationship to DTO inference and OpenAPI
The generated thread is also part of the broader contract-emission path.
That means generated entity, DTO, controller, and validation structure can feed:
- backend OpenAPI output
- DTO inference and generation
- frontend SDK generation
For the cross-stack side of this loop, also see Backend OpenAPI to Frontend SDK.
Generated workflow checklist
When you see a request like “create a student CRUD” or “scaffold backend resources,” the correct default should be:
- inspect the Vona CLI
- use
:tools:crudor:tools:crudBasicif one matches the need - modify the generated output instead of hand-building the whole thread from scratch
- verify the resulting migration, controller, and test path instead of stopping at file creation