# CRUD Pattern — Overview This document describes the full-stack CRUD pattern used across the dashboard. Every resource page (Customers, Vendors, Employees, etc.) is built from the same thin layer of generic, composable utilities. --- ## Architecture Layers ``` ┌──────────────────────────────────────────────────────────────────┐ │ Feature Page (apps/dashboard/app/…/page.tsx) │ │ • Declares columns, title, routeKey, getClient, renderForm │ └──────────────────┬───────────────────────────────────────────────┘ │ uses ┌──────────────────▼───────────────────────────────────────────────┐ │ ResourcePage (shared/data-view/resource-page) │ │ • Combines DashboardPage + FormDialog + DataTable │ │ • Delegates state to useResourcePage │ └──────┬───────────────────────────────┬────────────────────────────┘ │ │ ┌──────▼──────────────┐ ┌────────────▼───────────────────────────┐ │ useResourcePage │ │ DataTable (shared/data-view/table-view)│ │ • useDataTableQuery │ │ • TanStack Table (manual mode) │ │ • form dialog state │ │ • Pagination, sorting, skeleton │ │ • delete mutation │ │ • DataViewProvider context │ └──────┬──────────────┘ └────────────────────────────────────────┘ │ ┌──────▼──────────────────────────────────────────────────────────┐ │ useAuthApi → createApi() → CrudClient → openapi-fetch │ │ (packages/api — fully type-safe from OpenAPI schema) │ └─────────────────────────────────────────────────────────────────┘ │ ┌──────▼──────────────────────────────────────────────────────────┐ │ Feature Form (modules//-form.tsx) │ │ • useResourceForm (react-hook-form + Zod + optional fetch) │ │ • useMutation (create / update) │ │ • RhfTextField / RhfSelectField / RhfAsyncSelectField … │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Key Files | Layer | File | Purpose | |---|---|---| | Feature page | `app/(authenticated)/sales/customers/page.tsx` | Minimal feature config | | Generic page shell | `shared/data-view/resource-page/resource-page.tsx` | Layout + wiring | | Page logic hook | `shared/data-view/resource-page/use-resource-page.ts` | State + mutations | | Table | `shared/data-view/table-view/data-table.tsx` | TanStack Table UI | | Table data hook | `shared/data-view/table-view/use-data-table-query.ts` | React Query + URL state | | Form dialog | `shared/components/form-dialog.tsx` | URL-driven dialog trigger | | Confirm dialog | `shared/components/confirm-dialog.tsx` | Imperative async confirm | | Form state hook | `shared/hooks/use-resource-form.ts` | RHF + Zod + optional re-fetch | | Form field wrappers | `shared/components/form/` | RhfTextField, RhfSelectField, … | | API layer | `packages/api/src/infra/crud-client.ts` | Generic CRUD HTTP client | | Auth API hook | `shared/useApi.ts` | Creates authenticated API instance | --- ## Data Flow — Read (LIST) ``` URL params change (page / sort) → useDataTableQuery re-runs queryFn → client.list({ page, per_page, sort_by, sort_order }) → CrudClient.list() → ApiClient.get(indexRoute, { query }) → openapi-fetch GET /api/customers ← { data: [...], meta: { last_page, total, ... } } → DataTable renders rows + pagination ``` ## Data Flow — Write (CREATE / UPDATE) ``` User fills form → submits → useMutation mutationFn → api.customers.create(payload) or .update(id, payload) → CrudClient.create() / .update() → ApiClient.post() / .put() → openapi-fetch POST|PUT /api/customers[/{id}] ← 200/201 response → onSuccess: invalidateQuery() → list refreshes → onError: ApiError.validationErrors → form.setError(field, msg) ``` ## Data Flow — Delete ``` User clicks Delete in actions column → confirm({ title, description, variant: "destructive" }) → awaits boolean → if confirmed: deleteItem(id) → useMutation → client.destroy(id) → ApiClient.delete() ← success → invalidateQuery() ``` --- ## Creating a New Feature Page To add a new resource page following this pattern: 1. Create schema: `modules//.schema.ts` 2. Create form: `modules//-form.tsx` 3. Create page: `app/(authenticated)/
//page.tsx` The page needs only 5 props on ``: ```tsx title="My Resource" pageTitle="My Resources" routeKey={MY_ROUTES.INDEX} getClient={(api) => api.myClient} columns={({ actionsColumn }) => [ { accessorKey: "name", header: () => }, actionsColumn(), ]} renderForm={({ resourceId, initialData, onSuccess }) => ( )} /> ``` See the [Resource Page](./resource-page.md), [Data Fetching](./data-fetching.md), and [Form System](./form-system.md) docs for details on each layer.