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?
| Concern | Sites (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 client | Direct to KrakenD | Direct to KrakenD (same pattern) |
| External API consumers | Not supported | ✅ Via /api/v1/healthcare-practitioners |
What You'll Build
| Layer | File(s) | Purpose |
|---|---|---|
| Type | lib/types/practitioner.ts | TypeScript interface |
| Zod Schema | lib/schemas/practitioner-schema.ts | Runtime validation for create/update |
| Page | app/practitioners/page.tsx | Server Component + Suspense |
| Client | app/practitioners/practitioners-client.tsx | State, fetch, CRUD handlers |
| Table | components/practitioners/practitioners-table.tsx | TanStack Table |
| Form | components/practitioners/practitioner-form.tsx | Add/Edit form |
| API Route (list) | app/api/v1/healthcare-practitioners/route.ts | GET + POST with server auth |
| API Route (single) | app/api/v1/healthcare-practitioners/[id]/route.ts | GET + 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
| Schema | Used In | Purpose |
|---|---|---|
practitionerSchema | API response parsing | Validates data from Frappe |
createPractitionerSchema | POST /api/v1/healthcare-practitioners | Validates create request body |
updatePractitionerSchema | PUT /api/v1/healthcare-practitioners/[id] | Validates update request body |
practitionerListSchema | GET response | Validates 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:
- Parse and validate query params with Zod (
listQuerySchema) - Add
Authorization: token ${FRAPPE_API_TOKEN}header (server-side secret) - Make two calls to Frappe — one for count, one for paginated data
- Return consistent JSON envelope:
{ data, total, limit, offset } - Return Zod validation errors as
400if 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?
| Benefit | Explanation |
|---|---|
| Server-side auth | FRAPPE_API_TOKEN never exposed to the browser |
| Input validation | Zod catches bad inputs before they hit Frappe |
| Consistent API | External consumers (mobile app, integrations) get a clean REST API |
| Error handling | Maps Frappe errors to standard HTTP status codes |
| OpenAPI ready | JSDoc 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:
- Zod schemas → runtime input validation with typed error responses
- 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.