--- description: Main modules use separate read and write controllers with list/detail, CRUD, status, bulk, and CSV import globs: "src/modules/**/*.ts" alwaysApply: false --- # Read / Write Controllers ## Structure Every **main** (CRUD) feature module under `src/modules/` MUST expose at least: - `*-read.controller.ts` - `*-write.controller.ts` Same resource path and `@ApiTags`. Example: `@Controller('shipments')`. Exempt: auth and similar non-CRUD modules (single controller is fine). ```typescript @Module({ controllers: [ShipmentsReadController, ShipmentsWriteController], providers: [ShipmentsService, ShipmentsRepository], }) export class ShipmentsModule {} ``` Register static write paths (`import`, `bulk-delete`, `bulk-status`) **before** parameterized `:id` routes so they never collide. ## Read controller 1. `GET /` — list 2. `GET /:id` — detail List requirements: - Query filters for the resource’s own attributes **plus** `search` (case-insensitive match on the module’s searchable text columns; AND with other filters) - Shared pagination query (`page`/`limit` or `offset`/`limit`) plus `orderBy`/`orderType` via `PaginationQueryDto` - Handler **must** use `@Pagination()` and return `{ data, total }` — never build `meta` here (see `.cursor/rules/pagination-response.mdc`) - Service `visibleFields` whitelist: default **all non-secret** attributes; modules may narrow. Project in the **service**, not the controller - FK relations in list/detail (and write responses that reuse the mapper) MUST be nested objects via `pickRelation` — see `.cursor/rules/relation-response.mdc` - List query must be extendable (e.g. `extendListQuery(qb, filters)` on the repository/service) so joins/extra predicates can be added without forking list ## Write controller 1. `POST /` — create (status defaults to `draft` unless body sets a valid status) 2. `PATCH /:id` — update (**must not** change `status`; reject if `status` is present) 3. `DELETE /:id` — delete (hard delete unless the module documents otherwise) 4. `PATCH /:id/status` — body is **only** `{ status }` 5. `POST /bulk-delete` — `{ ids: string[] }` 6. `POST /bulk-status` — `{ ids: string[], status }` 7. `POST /import` — multipart CSV `file`; headers map to create fields; omitted status → `draft`; `created_by` / `updated_by` = current user; fail the batch on validation errors with row-level messages (do not echo raw invalid phones/dates beyond VO policy) Set `created_at` / `updated_at` (`DateTime`) and `created_by` / `updated_by` (current user id) in the write path. Document both controllers per `.cursor/rules/nestjs-swagger.mdc`.