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 typelib/types/site.tslib/types/practitioner.ts
Zod schemalib/schemas/practitioner-schema.ts
BFF API routesapp/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.