Raj Suhail

The form that could build itself

The backend said: here's the config, figure it out. What came next was one of the most interesting frontend problems I've worked on, not because it was flashy, but because getting it wrong would have been invisible, and getting it right had to feel effortless.

Main case study
01 — context

What we were building, and why the constraint mattered

5X is a data platform. At its core, it connects to your data sources (databases, SaaS tools, APIs) and centralises everything so teams can work with it. To do that, it needs to support a lot of connectors. Not ten. Not fifty. Hundreds.

We were using a third-party connector provider to handle the underlying integrations. Good decision: it offloaded a massive amount of infrastructure work. But it came with a specific challenge.

The connector configurations arrived from the third-party provider as structured data, well-formed, consistent in shape. The backend passed them through without transformation, which was the right call. Maintaining a translation layer between two evolving systems is a fragile bet. So the data arrived structured. But structured isn't the same as ready.

The config described what a connector needed: fields, types, rules, dependencies. What it didn't describe was how to present it. How to group related fields. How to sequence them in a way that felt logical to a human. How to turn a machine-readable spec into something a person could move through without thinking.

That gap, between structured data and a usable interface, was entirely mine to close.

02 — the problem

It wasn't one problem. It was five, stacked.

The first time I looked at the raw connector config, I had to resist the urge to ask the backend to "just add a bit more context." Instead I sat with it and mapped what I was actually dealing with.

Form Builder
Illustration of how Everything is mapped
  • 01

    Structure ≠ organisation

    The config was structured, but flat. No grouping, no visual hierarchy, no sense of what mattered first. Rendering it directly would have been a wall of inputs. The data knew what to ask; it didn't know how to ask it.

  • 02

    Every input type behaved differently

    Text, dropdowns, toggles, file uploads, multiselect, OAuth triggers: each needed its own component, its own validation logic, its own empty and error state. And some types I hadn't seen before in the wild.

  • 03

    Fields had dependants

    Some fields only appeared when another field had a specific value. Some changed their options based on a previous selection. The config encoded this as conditionals; I had to parse the logic and wire it to live UI state without it feeling janky.

  • 04

    OAuth was its own beast

    Some connectors authenticated via OAuth: a popup flow, a redirect, a token exchange. This had to be woven into the form experience seamlessly. Not a separate page. Not a jarring interruption. Just a button that handled the whole flow and returned the user back to where they were.

  • 05

    Validation couldn't be hardcoded

    Since every connector was different, validation rules came from the config too: required fields, formats, min/max lengths. I needed a validation engine that read from schema, not from a switch statement that grew forever.

  • 03 — the solution

    Before writing a component, I wrote a contract

    The instinct is to start building. But with a problem this wide, the order matters. If I started building field components without a clear schema contract, every new connector would expose a new edge case and I'd be patching forever.

    The config was already structured, so the normalisation step wasn't about cleaning up a mess. It was about making an intentional choice: what shape does the frontend actually need this data to be in? The third-party spec was designed for machines. My schema was designed for components.

    The normalisation layer was thin but deliberate. It took the provider's structure and mapped it to a typed internal schema: consistent field shapes, resolved conditionals, explicit ordering hints. From that point on, every component downstream spoke one language. The provider could change their format; only the normaliser would need to update.

    With the schema defined, I built the renderer, a React component that consumed the schema and returned a fully formed, interactive, validated form. It handled field ordering, grouping, conditional visibility, and field-level validation all from config. Adding a new connector didn't require touching the renderer at all.

    Normalisation layer

    A typed mapping step between the provider's spec and the frontend's schema. Thin, focused, easy to update. The only place that knew about the third-party format.

    Dependency engine

    Conditionals evaluated reactively: as the user typed or selected, dependent fields appeared, changed options, or disappeared. The form responded like it understood intent.

    OAuth integration

    OAuth was a field type in the schema. When the renderer hit one, it opened the popup, managed the token exchange, and wrote the result back into form state. One flow, no special cases.

    Validation

    Schema-driven validation using rules from the config. Errors were field-level, human-readable, and shown only after interaction, not on page load. Accessible by default.

    04 — THE CRAFT LAYER

    A config-driven form doesn't have to feel like one

    This is the part most engineering writeups skip. The system worked. But "working" is a low bar.

    Every field type got its own interaction design. Conditional fields didn't just appear: they slid in with a gentle entrance so the user understood something new was being revealed, not randomly materialising. Error states were written like a human was talking, not a validator throwing a code. The OAuth button had loading and success states that kept the user oriented through what is, under the hood, a fairly disorienting flow.

    The design decision that mattered most

    I spent a disproportionate amount of time on field ordering and grouping. Even though the config described what to ask, it didn't know how to ask it. Credentials first, optional settings last, related fields visually close together. That required reading the schema semantically, not just literally. No one told me to do this. It was just obviously right.

    05 — OUTCOMES

    What it unlocked

    300+supported connectors
    0frontend PRs per new connector
    1renderer for all of them

    The renderer became the foundation for the entire integrations surface at 5X: not just initial setup forms, but configuration editing, credential management, and re-authentication flows. Features that would have taken a sprint each now took hours, because the infrastructure was already there.

    More importantly, users didn't notice it. Which was exactly the goal.

    06 — REFLECTION

    What I'd do differently

    Schema versioning

    When the third-party provider updated their format, it quietly stressed the normalisation layer. I'd version the schema contract from day one and treat breaking changes like API changes: explicitly, not as surprises.

    A config preview tool, earlier

    I eventually built a small internal tool so engineers could preview how a config would render. I built it too late. If it existed from the start, edge cases would have surfaced before QA.

    The deeper lesson: structured data is a starting point, not a solution. The gap between what a machine describes and what a human experiences, that's the design problem. And it's always worth taking seriously.