Skip to main content

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​

LayerFile(s)Purpose
Typelib/types/site.tsTypeScript interface for the DocType
Pageapp/sites/page.tsxServer Component + Suspense
Clientapp/sites/sites-client.tsxState, fetch, CRUD handlers
Tablecomponents/sites/sites-table.tsxTanStack Table with sort, search, pagination
Formcomponents/sites/site-form.tsxAdd/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​

ResponsibilityImplementation
Fetch listfetchFrappeDoctypePaginated<ApiSite>({ doctype: "Site", fields, page, ... })
SearchURL param ?search= → triggers re-fetch
PaginationURL-driven ?page=, ?pageSize= → router.push
CreatePOST ${apiBaseUrl}/doctype/Site
UpdatePUT ${apiBaseUrl}/doctype/Site/${name}
DeleteDELETE ${apiBaseUrl}/doctype/Site/${name}
Bulk DeletePromise.allSettled over individual DELETE calls
RBACuseActionPermissions() → canCreate('site'), canUpdate('site'), canDelete('site')
Page headerusePageHeader() → title, subtitle, icon (Building2)
Error stateRenders 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/​

ComponentKey Features
sites-table.tsxTanStack Table, column sorting, row selection, search input, inline action menu (edit/delete), DataTablePagination, ActionGuard for RBAC
site-form.tsxSlide-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.