Basic CRUD — Sites
The Sites module is the simplest entity in the platform — a single Frappe DocType with straightforward CRUD, no schema validation layer, and no cross-service dependencies. Start here to understand the minimal set of files needed for a new entity.
What You'll Build
| Layer | File(s) | Purpose |
|---|---|---|
| Type | lib/types/site.ts | TypeScript interface for the DocType |
| Page | app/sites/page.tsx | Server Component + Suspense |
| Client | app/sites/sites-client.tsx | State, fetch, CRUD handlers |
| Table | components/sites/sites-table.tsx | TanStack Table with sort, search, pagination |
| Form | components/sites/site-form.tsx | Add/Edit slide-over form |
No Zod schema, no service class, no BFF API routes — just 5 files.
1. Type — lib/types/site.ts
export type ApiSite = {
name: string
site_name: string
expected_number_of_participants?: number
expected_start_date?: string
irb_approval_date?: string
lead_investigator?: string
status?: string
site_location?: string
modified_by?: string
creation?: string
modified?: string
}
This is a plain TypeScript type — no Zod, no runtime validation. It mirrors the Frappe Site DocType fields.
2. Page Route — app/sites/page.tsx
import { Suspense } from "react"
import { ApiSitesClient } from "./sites-client"
export default function ApiSitesPage() {
return (
<div className="container mx-auto py-8 pl-6 pr-6">
<Suspense fallback={<div>Loading...</div>}>
<ApiSitesClient />
</Suspense>
</div>
)
}
A Server Component that does nothing except wrap the client in <Suspense>. Every entity page follows this exact pattern.
3. Client Container — app/sites/sites-client.tsx
The "use client" component that owns everything:
Configuration Constants
const SITE_FIELDS = [
"name", "site_name", "expected_number_of_participants",
"expected_start_date", "irb_approval_date",
"lead_investigator", "status", "site_location",
"modified", "creation",
]
const SITE_SEARCH_FIELDS = ["site_name", "name", "lead_investigator", "status"]
const DEFAULT_ORDER_BY = "modified desc"
Responsibilities
| Responsibility | Implementation |
|---|---|
| Fetch list | fetchFrappeDoctypePaginated<ApiSite>({ doctype: "Site", fields, page, ... }) |
| Search | URL param ?search= → triggers re-fetch |
| Pagination | URL-driven ?page=, ?pageSize= → router.push |
| Create | POST ${apiBaseUrl}/doctype/Site |
| Update | PUT ${apiBaseUrl}/doctype/Site/${name} |
| Delete | DELETE ${apiBaseUrl}/doctype/Site/${name} |
| Bulk Delete | Promise.allSettled over individual DELETE calls |
| RBAC | useActionPermissions() → canCreate('site'), canUpdate('site'), canDelete('site') |
| Page header | usePageHeader() → title, subtitle, icon (Building2) |
| Error state | Renders error card with "Try Again" button |
Data Flow
All CRUD calls go directly to the KrakenD API Gateway (apiBaseUrl) which proxies to Frappe with authentication. No BFF API routes needed.
4. UI Components — components/sites/
| Component | Key Features |
|---|---|
sites-table.tsx | TanStack Table, column sorting, row selection, search input, inline action menu (edit/delete), DataTablePagination, ActionGuard for RBAC |
site-form.tsx | Slide-over form for create/edit with field inputs |
Request Flow — Listing
Request Flow — Creating
File Structure Summary
src/
├── app/sites/
│ ├── page.tsx ← Server Component
│ └── sites-client.tsx ← Client Component (all logic)
├── components/sites/
│ ├── sites-table.tsx ← TanStack Table
│ └── site-form.tsx ← Add/Edit form
└── lib/types/
└── site.ts ← TypeScript type
Key Takeaway
Sites demonstrates the minimum viable entity — just a type, a page, a client component, a table, and a form. If your new entity is a simple single-DocType CRUD screen, follow this exact pattern. For entities that need input validation, API route handlers, or server-side logic, see CRUD with Validation — Practitioners.