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.