Configurable forms

Forms defined by configuration

Every form was a hand-written screen. For the modules that change most between projects, the form became a schema the frontend renders and the backend validates.

The problem
Hand-written forms per module, when each project wanted different fields.
The decision
The form as data: fields, sections and rules stored as configuration and validated on the backend too.
How it ended
Forms in those modules are adjusted per project without code changes.

The problem

The platform's tracking modules each had their own hand-written create and edit form. For some clinical modules that wasn't enough: each project needed different fields, different sections and rules like "this field only appears if that one has a given value". Doing it in code meant a change and a deploy per project.

Decisions

Configuration instead of code. The team defined a form as a record with its fields, sections and settings, and responses are stored linked to the item they belong to, as drafts or submitted. Cost: the database doesn't check the shape of the responses; that job moves to the application.

Rules are data too, and they're validated on the server. A field can be shown or required depending on the value of others. I added the evaluation of those rules to the read-only view. The backend validates every response again, so the rules don't depend only on the browser. Cost: the same logic lives on the frontend and the backend, and they have to be kept aligned.

A shared builder. The team had built a drag-and-drop builder inside one module. I turned it into a shared component, added section management and connected it to an external clinical data integration. Cost: the builder's state grew quite a bit.

Warn instead of overwrite. Several people edit the same record. If someone else saved first, the user sees who changed it and doesn't lose what they typed. Cost: the conflict is resolved by hand.

Outcome

  • In those modules, adding a field or a rule doesn't need a deploy.
  • The other modules still use their own forms; we didn't migrate them.

What I'd do differently

I'd define the schema contract once and share it between builder, view and validation, instead of each layer having its own version.