4 min read Emadideen Ghannam

The dynamic form-rendering engine that paid for itself in a quarter

Eighty percent of an enterprise SaaS is forms. Build the form engine once and the rest is configuration.

Eight years ago I was rebuilding a Supply Chain Management platform on Angular 6. The team had twelve modules to ship. Each module had between five and twenty forms. Each form had between three and forty fields. Conservative estimate: 600 distinct form fields across the platform, each with their own validation, layout, and conditional behaviour.

Three engineers, six months. We built the platform. We did not write 600 form fields by hand. We wrote a form-rendering engine that interpreted JSON schemas at runtime. Every module after the third one was 50 percent faster to ship.

This is the pattern.

The shape

A form-rendering engine takes a schema and returns a fully-validated, fully-styled form component. The schema describes:

  • The fields, by type and name.
  • The validators per field.
  • The conditional visibility (show this field if that one is true).
  • The layout (one column, two columns, sections, tabs).
  • The actions (submit, draft, cancel).
const schema: FormSchema = {
  fields: [
    {
      name: 'employeeId',
      type: 'text',
      label: 'Employee ID',
      validators: [{ kind: 'required' }, { kind: 'pattern', value: /^EMP-\d{4}$/ }],
    },
    {
      name: 'department',
      type: 'select',
      label: 'Department',
      options: { source: 'api', endpoint: '/api/departments' },
      validators: [{ kind: 'required' }],
    },
    {
      name: 'managerId',
      type: 'select',
      label: 'Manager',
      options: { source: 'api', endpoint: '/api/users?role=manager' },
      visibleIf: { field: 'department', equals: 'engineering' },
    },
  ],
  layout: { type: 'two-column', sections: ['Identity', 'Org'] },
};

The engine consumes that, builds an Angular reactive form, renders the layout, wires the validators, and listens for changes to drive the conditional visibility.

Why it pays for itself

The first form takes a week. You’re building the engine, not the form. The second form takes a day. You’re filling in the missing pieces. The third form takes two hours. By the tenth form, you’re asking the product manager why they wrote a Jira ticket for something that should have been a config change.

The economics are obvious in retrospect. A platform with 600 form fields and a hand-written form per module pays linearly. A platform with the same 600 fields and a form engine pays once and then the marginal cost of a new form is the cost of writing JSON.

In our case the engine paid for itself in a quarter and continued paying for the next four years.

What kills these engines

Three failure modes I have seen, each fatal.

1. The engine becomes a parallel framework. The team ships a feature, the form needs a custom date picker that the engine doesn’t support, so they extend the engine. They extend it again for a multi-step form. They extend it again for a wizard. After a year the engine is an Angular reimplementation with all the complexity and none of the documentation. Avoid this by treating the engine as constrained on purpose. If a form needs something the engine can’t express, you write that form by hand. The engine handles 80 percent of the cases. It is not supposed to handle 100.

2. The schemas live in the wrong place. If the schemas live in the frontend repo, the backend can’t validate against them. If they live in the backend repo, the frontend has no design-time autocomplete. The right answer is a shared schema package that both sides import. Not a code-generated bundle from OpenAPI; a real TypeScript or JSON Schema package that lives in libs/contracts or equivalent.

3. Validation drifts between client and server. The engine validates on the client. The server validates with its own logic. Six months later they disagree on what’s valid. Use the same validators on both sides, or generate one from the other. Otherwise users will get a green tick on the client and a 400 on save.

What I’d build differently today

If I were starting over in 2026:

  • I’d use TypeScript types as the source of truth, not JSON schemas. Modern Angular’s type-safety is good enough that the form engine can be fully typed. The schema is just the runtime form of the type.
  • I’d lean on signals for the conditional visibility logic instead of RxJS. Cleaner, easier to reason about.
  • I’d ship the engine as an internal npm package from day one, even for a single product. The discipline of “this is a library, not a folder of components” forces the boundary stays clean.
  • I’d write a single end-to-end test for each form schema rather than unit-testing the engine. The unit tests rot. The end-to-end tests catch what users will catch.

Where this pattern wins and where it doesn’t

Wins: enterprise CRUD apps, internal admin panels, multi-tenant SaaS where every customer has slightly different forms, anything where forms outnumber custom interactions.

Loses: consumer products where the form is the experience (signup, checkout). Those forms are too design-driven for an engine. Build them by hand with care.


Edit on GitHub (opens in new tab)