2026-03-27 16:03:58 +03:00

6.5 KiB

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/<feature>/<feature>-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/<feature>/<feature>.schema.ts
  2. Create form: modules/<feature>/<feature>-form.tsx
  3. Create page: app/(authenticated)/<section>/<feature>/page.tsx

The page needs only 5 props on <ResourcePage>:

<ResourcePage<MyClient>
  title="My Resource"
  pageTitle="My Resources"
  routeKey={MY_ROUTES.INDEX}
  getClient={(api) => api.myClient}
  columns={({ actionsColumn }) => [
    { accessorKey: "name", header: () => <ColumnHeader ... /> },
    actionsColumn(),
  ]}
  renderForm={({ resourceId, initialData, onSuccess }) => (
    <MyForm resourceId={resourceId} initialData={initialData} onSuccess={onSuccess} />
  )}
/>

See the Resource Page, Data Fetching, and Form System docs for details on each layer.