Skip to main content

CRUD with Validation — Practitioners

The Healthcare Practitioners module builds on the basic Sites pattern by adding two important layers: Zod schema validation and BFF API route handlers. This is the recommended pattern for entities that will be consumed by external integrations or need server-side input validation.


What's Different from Sites?​

ConcernSites (Basic)Practitioners (Advanced)
TypeScript type✅ lib/types/site.ts✅ lib/types/practitioner.ts
Zod schema❌✅ lib/schemas/practitioner-schema.ts
BFF API routes❌✅ app/api/v1/healthcare-practitioners/
CRUD calls from clientDirect to KrakenDDirect to KrakenD (same pattern)
External API consumersNot supported✅ Via /api/v1/healthcare-practitioners

What You'll Build​

LayerFile(s)Purpose
Typelib/types/practitioner.tsTypeScript interface
Zod Schemalib/schemas/practitioner-schema.tsRuntime validation for create/update
Pageapp/practitioners/page.tsxServer Component + Suspense
Clientapp/practitioners/practitioners-client.tsxState, fetch, CRUD handlers
Tablecomponents/practitioners/practitioners-table.tsxTanStack Table
Formcomponents/practitioners/practitioner-form.tsxAdd/Edit form
API Route (list)app/api/v1/healthcare-practitioners/route.tsGET + POST with server auth
API Route (single)app/api/v1/healthcare-practitioners/[id]/route.tsGET + PUT + DELETE

8 files total — 3 more than the basic pattern.


1. Type — lib/types/practitioner.ts​

export type ApiPractitioner = {
name?: string
department: string
first_name: string
last_name: string
gender: string
hospital: string
mobile_phone: string
office_phone: string
status: string
education?: string
experience?: number
biography?: string
surgeries?: number
image?: string
}

2. Zod Schema — lib/schemas/practitioner-schema.ts (NEW)​

This is the key addition. Zod schemas provide runtime validation for API inputs.

import { z } from 'zod';

// Base schema — describes the full shape of a practitioner
export const practitionerSchema = z.object({
name: z.string(),
practitioner_name: z.string(),
first_name: z.string().optional(),
last_name: z.string().optional(),
gender: z.enum(['Male', 'Female', 'Other']).optional(),
mobile_phone: z.string().optional(),
email: z.string().email().optional(),
department: z.string().optional(),
designation: z.string().optional(),
speciality: z.string().optional(),
status: z.enum(['Active', 'Disabled', 'Inactive']).optional(),
});

// Create — strict validation for new records
export const createPractitionerSchema = z.object({
practitioner_name: z.string().min(1, 'Practitioner name is required'),
first_name: z.string().optional(),
last_name: z.string().optional(),
gender: z.enum(['Male', 'Female', 'Other']).optional(),
mobile_phone: z.string().optional(),
email: z.string().email('Invalid email address').optional(),
department: z.string().optional(),
});

// Update — all fields optional (partial update)
export const updatePractitionerSchema = createPractitionerSchema.partial();

// List response — wraps an array of practitioners
export const practitionerListSchema = z.object({
data: z.array(practitionerSchema),
});

Schema Convention​

SchemaUsed InPurpose
practitionerSchemaAPI response parsingValidates data from Frappe
createPractitionerSchemaPOST /api/v1/healthcare-practitionersValidates create request body
updatePractitionerSchemaPUT /api/v1/healthcare-practitioners/[id]Validates update request body
practitionerListSchemaGET responseValidates list response shape

3. BFF API Routes (NEW)​

These Next.js Route Handlers act as a Backend-for-Frontend proxy — they add server-side authentication and Zod validation before forwarding to Frappe.

app/api/v1/healthcare-practitioners/route.ts​

GET  /api/v1/healthcare-practitioners     → List (paginated)
POST /api/v1/healthcare-practitioners → Create

What the handler does:

  1. Parse and validate query params with Zod (listQuerySchema)
  2. Add Authorization: token ${FRAPPE_API_TOKEN} header (server-side secret)
  3. Make two calls to Frappe — one for count, one for paginated data
  4. Return consistent JSON envelope: { data, total, limit, offset }
  5. Return Zod validation errors as 400 if params are invalid

app/api/v1/healthcare-practitioners/[id]/route.ts​

GET    /api/v1/healthcare-practitioners/{id}   → Read
PUT /api/v1/healthcare-practitioners/{id} → Update
DELETE /api/v1/healthcare-practitioners/{id} → Delete

Why BFF Routes?​

BenefitExplanation
Server-side authFRAPPE_API_TOKEN never exposed to the browser
Input validationZod catches bad inputs before they hit Frappe
Consistent APIExternal consumers (mobile app, integrations) get a clean REST API
Error handlingMaps Frappe errors to standard HTTP status codes
OpenAPI readyJSDoc annotations enable API documentation generation

4. Page, Client, Table, Form​

These follow the exact same pattern as Sites. The only difference is the DocType name and fields:

// In practitioners-client.tsx
const result = await fetchFrappeDoctypePaginated<ApiPractitioner>({
doctype: "Healthcare Practitioner", // ← DocType name
fields: PRACTITIONER_FIELDS,
page, pageSize, search,
searchFields: PRACTITIONER_SEARCH_FIELDS,
orderBy: DEFAULT_ORDER_BY,
})

CRUD mutations also go directly to KrakenD (same as Sites):

// Create
await fetch(`${apiBaseUrl}/doctype/Healthcare Practitioner`, { method: 'POST', ... })

// Update
await fetch(`${apiBaseUrl}/doctype/Healthcare Practitioner/${name}`, { method: 'PUT', ... })

// Delete
await fetch(`${apiBaseUrl}/doctype/Healthcare Practitioner/${name}`, { method: 'DELETE' })

Request Flow — External API Consumer​

This flow shows how the BFF API routes serve external clients (mobile app, integrations):

Request Flow — Frontend CRUD​

The UI client calls KrakenD directly (same as Sites):


File Structure Summary​

src/
├── app/
│ ├── practitioners/
│ │ ├── page.tsx ← Server Component
│ │ └── practitioners-client.tsx ← Client Component
│ └── api/v1/healthcare-practitioners/ ← BFF API Routes
│ ├── route.ts ← GET (list) + POST (create)
│ └── [id]/route.ts ← GET + PUT + DELETE
├── components/practitioners/
│ ├── practitioners-table.tsx ← TanStack Table
│ └── practitioner-form.tsx ← Add/Edit form
└── lib/
├── types/practitioner.ts ← TypeScript type
└── schemas/practitioner-schema.ts ← Zod validation schemas

Key Takeaway​

Practitioners adds two layers on top of the basic Sites pattern:

  1. Zod schemas → runtime input validation with typed error responses
  2. BFF API routes → server-side auth proxy for external consumers

Use this pattern when your entity needs to be accessed by external clients (mobile app, integrations) or when you want server-side validation before data reaches Frappe.