04-clinicalwave: W2filled11 citations

Clinical Forms Builder — Dynamic Assessment Form Engine

Audiences: doctor, nurse, clinical-buyer, developer, internal

Clinical Forms Builder — Dynamic Assessment Form Engine

A two-tier forms system: assessment-forms for structured FHIR-backed clinical questionnaires (definition + versioning + instance lifecycle), and forms for 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:

  1. Assessment Forms (/api/v1/assessment-forms) — a versioned form definition and instance management system backed by FHIR Questionnaire and QuestionnaireResponse resources. 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.

  2. Dynamic Forms (/api/v1/forms) — an event-driven submission processor. Forms are identified by formType and routed to registered FormHandler implementations. 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). FHIR Questionnaire and QuestionnaireResponse synced via descriptors. Form instance status: DRAFT → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED.

Architecture

Assessment Forms (structured FHIR-backed)

diagram
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)

diagram
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

LayerTechnologyNotes
Form definitionsPrisma ddx_api_mainAssessmentForm, AssessmentFormVersion, AssessmentFormPage, AssessmentFormField
FHIR syncHAPI FHIR Questionnaire + QuestionnaireResponseVia questionnaire.descriptor.ts + questionnaire-response.descriptor.ts
ValidationZod v4 (import { z } from 'zod/v4')Controller-level schema validation for all DTOs
Auth@ProtectedRead, @ProtectedWrite, @ProtectedManageCustom permission decorators scoped to 'assessment-forms' resource
Public accessPublicFormAccessService + standalone tokensGuest form submission without authentication
Instance statusFormInstanceStatus enumDRAFT → IN_PROGRESS → SUBMITTED → APPROVED/REJECTED/CANCELLED
Dynamic handlersFormHandlerRegistryMap-based registry; handlers implement BaseFormHandler interface
AnalyticsFormAnalyticsServiceSubmission 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:

  1. POST /api/v1/assessment-forms with { title: "PHQ-9", description: "Depression Screener" }AssessmentFormsController.create()AssessmentFormCrudService.createDefinition(). Form created as DRAFT.
  2. Admin adds pages/groups/fields via PUT /api/v1/assessment-forms/:id/definitionAssessmentFormDefinitionStructureService.
  3. POST /api/v1/assessment-forms/:id/publishAssessmentFormPublishService transitions form to PUBLISHED state. A QuestionnaireDescriptor syncs the definition to HAPI FHIR as a Questionnaire resource.
  4. 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.

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:13import { z } from 'zod/v4' — Zod v4 validation (NestJS canonical import)
  • ddx-api/src/clinical-forms/assessment-forms/assessment-forms.controller.ts:51AssessmentFormsController@ProtectedRead/Write/Manage('assessment-forms') decorators
  • ddx-api/src/clinical-forms/assessment-forms/assessment-forms.controller.ts:61list()gatewayUserSchema.parse(rawUser) for user validation
  • ddx-api/src/clinical-forms/assessment-forms/assessment-forms.service.ts:1 — header comment documenting 5 sub-services
  • ddx-api/src/clinical-forms/assessment-forms/assessment-forms.service.ts:28AssessmentFormsService — thin facade pattern over AssessmentFormCrudService, AssessmentFormPatchService, AssessmentFormPublishService, AssessmentFormBuilderService, AssessmentFormFieldService
  • ddx-api/src/clinical-forms/assessment-forms/questionnaire.descriptor.ts:1 — FHIR Questionnaire descriptor
  • ddx-api/src/clinical-forms/assessment-forms/questionnaire-response.descriptor.ts:1 — FHIR QuestionnaireResponse descriptor
  • ddx-api/src/clinical-forms/assessment-forms/assessment-form-instances.controller.ts:1AssessmentFormInstancesController — instance lifecycle management
  • ddx-api/src/clinical-forms/forms/forms.controller.ts:27@RbacExempt() — dynamic forms accessible to all authenticated roles
  • ddx-api/src/clinical-forms/forms/forms.controller.ts:44POST /forms/submit — dynamic form submission entry point
  • ddx-api/src/clinical-forms/forms/form-handler.registry.ts:1FormHandlerRegistry — formType → handler routing

Operational Notes

  • Zod v4 import: Assessment forms controller uses import { z } from 'zod/v4' — the canonical NestJS import. Using from 'zod' (Next.js form) in this module would cause Zod v3/v4 version conflicts. Never mix imports.
  • Public form tokens: PublicFormAccessService manages 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 formType string. If a submitted formType has no registered handler, FormHandlerRegistry throws — returns 500. Ensure all deployed formType values have corresponding handler implementations.
  • Instance status machine: FormInstanceStatus transitions are enforced — a SUBMITTED instance cannot go back to DRAFT. Review state guard logic in FormInstanceSubmitService before adding new status transitions.
  • FHIR sync on publish: Publishing a form definition triggers a FHIR Questionnaire resource write. Verify HAPI FHIR connectivity before publishing in production.
  • 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
    Clinical Forms Builder — Dynamic Assessment Form Engine — Dudoxx Docs | Dudoxx