Clinical Forms Builder — Dynamic Assessment Form Engine
Audiences: doctor, nurse, clinical-buyer, developer, internal
A two-tier forms system:
assessment-formsfor structured FHIR-backed clinical questionnaires (definition + versioning + instance lifecycle), andformsfor dynamic event-driven form submission processing (consent, intake triggers, etc.).
Business Purpose
Clinical workflows require structured data collection beyond free-text notes. Intake questionnaires, pain assessment scales, PHQ-9 depression screeners, functional assessments, consent forms, and medication checklists are all "forms." Without a configurable forms engine, each use case requires a custom-coded screen.
Dudoxx HMS provides two complementary form systems:
-
Assessment Forms (
/api/v1/assessment-forms) — a versioned form definition and instance management system backed by FHIRQuestionnaireandQuestionnaireResponseresources. Clinical administrators define forms with pages, groups, and fields; clinicians fill instances per patient; the system tracks submission status, supports public (guest) form tokens, and publishes via a semantic versioning lifecycle. -
Dynamic Forms (
/api/v1/forms) — an event-driven submission processor. Forms are identified byformTypeand routed to registeredFormHandlerimplementations. Used for consent captures, intake triggers, and similar event-driven workflows. All authenticated roles can submit; the handler registry routes to the correct processor.
Together, these enable clinics to add new structured data collection workflows through configuration, not code.
Audiences
- Investor: Configurable clinical forms eliminate the "custom development for every form" fee argument that competing platforms use to lock in customers. Clinics can deploy a new assessment in minutes.
- Clinical buyer (doctor/nurse/receptionist): Admins build forms in the UI; clinicians fill them during or after consultations; patients can fill public forms (pre-registration, consent) without logging in via public form tokens.
- Developer/partner: Assessment forms:
POST /api/v1/assessment-forms(create definition),POST /api/v1/assessment-forms/:id/publish(publish version),POST /api/v1/assessment-form-instances(fill form). Dynamic forms:POST /api/v1/forms/submit(submit by formType). Zod v4 schemas used for DTO validation (import { z } from 'zod/v4'). - Internal (ops/support): Form definitions in Prisma
ddx_api_main(AssessmentForm,AssessmentFormVersion,AssessmentFormPage,AssessmentFormField). FHIRQuestionnaireandQuestionnaireResponsesynced via descriptors. Form instance status:DRAFT → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED.
Architecture
Assessment Forms (structured FHIR-backed)
AssessmentFormsController (/api/v1/assessment-forms)
└── AssessmentFormsService — thin facade over 5 sub-services
├── AssessmentFormCrudService — list, create, get definitions
├── AssessmentFormPatchService — patch (status, fields)
├── AssessmentFormPublishService — semantic versioning lifecycle
├── AssessmentFormBuilderService — pages + groups + section reordering
└── AssessmentFormFieldService — add/remove/update individual fields
AssessmentFormInstancesController (/api/v1/assessment-form-instances)
└── AssessmentFormInstancesService
├── FormInstanceQueryService — queries by patient, form, status
├── FormInstanceSubmitService — submission processing
├── FormInstanceReviewService — approval/rejection workflow
└── FormInstancePatientService — patient-facing instance access
↓ FHIR descriptors
QuestionnaireDescriptor — FHIR Questionnaire resource management
QuestionnaireResponseDescriptor — FHIR QuestionnaireResponse resource management
Dynamic Forms (event-driven)
FormsController (/api/v1/forms)
└── FormsService
└── FormHandlerRegistry — maps formType string → FormHandler instance
└── handlers/ — one handler class per formType
@RbacExempt() on FormsController — all authenticated roles may submit dynamic forms.
Tech Stack & Choices
| Layer | Technology | Notes |
|---|---|---|
| Form definitions | Prisma ddx_api_main | AssessmentForm, AssessmentFormVersion, AssessmentFormPage, AssessmentFormField |
| FHIR sync | HAPI FHIR Questionnaire + QuestionnaireResponse | Via questionnaire.descriptor.ts + questionnaire-response.descriptor.ts |
| Validation | Zod v4 (import { z } from 'zod/v4') | Controller-level schema validation for all DTOs |
| Auth | @ProtectedRead, @ProtectedWrite, @ProtectedManage | Custom permission decorators scoped to 'assessment-forms' resource |
| Public access | PublicFormAccessService + standalone tokens | Guest form submission without authentication |
| Instance status | FormInstanceStatus enum | DRAFT → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED/CANCELLED |
| Dynamic handlers | FormHandlerRegistry | Map-based registry; handlers implement BaseFormHandler interface |
| Analytics | FormAnalyticsService | Submission counts, completion rates per form definition |
Key design choice: Assessment forms use @ProtectedRead/Write/Manage custom decorators (scoped to 'assessment-forms' permission resource) rather than raw @RequireRole. This allows fine-grained permission configuration via the RBAC permission catalog without hardcoding roles in controller decorators.
Data Flow
Admin creates a form definition and publishes it
Business outcome: Clinic admin creates a PHQ-9 depression screener form; it becomes available for clinicians to assign to patients.
Technical mechanism:
POST /api/v1/assessment-formswith{ title: "PHQ-9", description: "Depression Screener" }→AssessmentFormsController.create()→AssessmentFormCrudService.createDefinition(). Form created asDRAFT.- Admin adds pages/groups/fields via
PUT /api/v1/assessment-forms/:id/definition→AssessmentFormDefinitionStructureService. POST /api/v1/assessment-forms/:id/publish→AssessmentFormPublishServicetransitions form toPUBLISHEDstate. AQuestionnaireDescriptorsyncs the definition to HAPI FHIR as aQuestionnaireresource.- Clinicians can now create instances (
POST /api/v1/assessment-form-instances).
Clinician assigns form to patient
POST /api/v1/assessment-form-instances with { formId, patientId, assignedBy } → AssessmentFormInstancesService creates an AssessmentFormInstance Prisma row in DRAFT status. FHIR QuestionnaireResponse resource created via QuestionnaireResponseDescriptor.
Dynamic form submission (consent capture)
POST /api/v1/forms/submit with { formId, formType: "consent-intake", userId, organizationId, fieldValues: {...} } → FormsService.submitForm() → FormHandlerRegistry resolves handler by formType → handler processes submission (triggers downstream actions, creates records, fires SSE events).
Implicated Code
ddx-api/src/clinical-forms/assessment-forms/assessment-forms.controller.ts:13—import { z } from 'zod/v4'— Zod v4 validation (NestJS canonical import)ddx-api/src/clinical-forms/assessment-forms/assessment-forms.controller.ts:51—AssessmentFormsController—@ProtectedRead/Write/Manage('assessment-forms')decoratorsddx-api/src/clinical-forms/assessment-forms/assessment-forms.controller.ts:61—list()—gatewayUserSchema.parse(rawUser)for user validationddx-api/src/clinical-forms/assessment-forms/assessment-forms.service.ts:1— header comment documenting 5 sub-servicesddx-api/src/clinical-forms/assessment-forms/assessment-forms.service.ts:28—AssessmentFormsService— thin facade pattern overAssessmentFormCrudService,AssessmentFormPatchService,AssessmentFormPublishService,AssessmentFormBuilderService,AssessmentFormFieldServiceddx-api/src/clinical-forms/assessment-forms/questionnaire.descriptor.ts:1— FHIR Questionnaire descriptorddx-api/src/clinical-forms/assessment-forms/questionnaire-response.descriptor.ts:1— FHIR QuestionnaireResponse descriptorddx-api/src/clinical-forms/assessment-forms/assessment-form-instances.controller.ts:1—AssessmentFormInstancesController— instance lifecycle managementddx-api/src/clinical-forms/forms/forms.controller.ts:27—@RbacExempt()— dynamic forms accessible to all authenticated rolesddx-api/src/clinical-forms/forms/forms.controller.ts:44—POST /forms/submit— dynamic form submission entry pointddx-api/src/clinical-forms/forms/form-handler.registry.ts:1—FormHandlerRegistry— formType → handler routing
Operational Notes
- Zod v4 import: Assessment forms controller uses
import { z } from 'zod/v4'— the canonical NestJS import. Usingfrom 'zod'(Next.js form) in this module would cause Zod v3/v4 version conflicts. Never mix imports. - Public form tokens:
PublicFormAccessServicemanages standalone tokens for guest submissions (unauthenticated patient-facing forms). Tokens are time-limited and scoped to a specific form definition. Verify token validation logic before exposing public intake URLs to the internet. - Form handler registry: Dynamic forms route by
formTypestring. If a submittedformTypehas no registered handler,FormHandlerRegistrythrows — returns 500. Ensure all deployedformTypevalues have corresponding handler implementations. - Instance status machine:
FormInstanceStatustransitions are enforced — aSUBMITTEDinstance cannot go back toDRAFT. Review state guard logic inFormInstanceSubmitServicebefore adding new status transitions. - FHIR sync on publish: Publishing a form definition triggers a FHIR
Questionnaireresource write. Verify HAPI FHIR connectivity before publishing in production.
Related Topics
- Assessments — Assessment forms are the primary delivery mechanism for clinical evaluation scales
- Intake Engine — Dynamic forms used for intake triggers; assessment forms for structured intake questionnaires
- Clinical Visits — Form instances are often linked to a visit
- Clinical Activities — Form submissions appear in patient activity timeline