Written by Abdul Rafay 8 min read

Weaving DNA Into a Living Form Builder

Every once in a while, you inherit a greenfield problem that feels equal parts thrilling and mildly panic-inducing. And before we even get into it… yes, I know. I haven’t written a blog post in almost two months. Turns out once you get an actual job and start shipping real features (and occasionally breaking production 🤫), time disappears faster than your console logs during a refactor.

But I’m a “real dev” now — pushing code, fixing bugs, summoning TypeScript errors like Pokémon — so naturally, my first post after this drought had to be about something dramatic.

And ours was dramatic: build a vendor onboarding experience where nothing is hardcoded, every service type invents its own rules, and the UI, validation, and submission logic all magically assemble themselves from server-delivered JSON we lovingly called DNA.

This is the story of how that system came to life inside webapp_frontend, what it looks like today, and what I learned while making it production-ready (and not crying… most days).

The Blank Canvas (But Make It Relatable)

When we kicked things off, there was no schema, no wizard, and definitely no guardrails. It was the kind of blank canvas that sounds empowering until you realize you’re the one expected to paint the Mona Lisa with a broken brush and a deadline.

Product came to us with requirements that basically said:

In other words: Do not hardcode anything Do not assume anything Do not break anything (haha)

Hard-coding forms or turning the app into a graveyard of feature flags would’ve collapsed instantly. We needed a lightweight contract that could describe every tiny variation without turning us into part-time archaeologists digging through JSX conditionals.

So we created our hero: DNA.

What DNA Actually Encodes (aka JSON With Superpowers)

DNA is fetched per service type through useDNALoader, and it arrives wrapped as a DNAConfig object (src/types/dna.ts). Think of it as a tiny JSON-powered design system that politely tells the UI what to do, how to behave, and when to panic.

It defines things like:

Since React doesn’t know (or care) what service type is being registered, the entire personality of the form comes from interpreting this DNA.

It’s basically configuration-driven UI on steroids — but without becoming unmaintainable spaghetti, because all the logic lives in well-defined hooks rather than scattered if conditions.

Lifecycle at a Glance

graph TD
    Wizard[ServiceRegistrationWizard] -->|serviceTypeId| Loader[useDNALoader]
    Loader -->|DNAConfig| Config[useDnaFormConfig]
    Config -->|builders + validators| State[DynamicServiceFormStep state]
    State -->|per-step data| UI[DynamicField & specialized sections]
    UI -->|updates| State
    State -->|validated payload| FormData[buildServiceFormData]
FormData --> API[Vendor API]

This loop means new DNA drops from the backend without a single code change on the frontend.

Spoiler: Geek-Out Ahead (Developer Mode Activated)

Okay, buckle up — this is the part where we stop pretending this is a “simple” form system and admit it’s basically a miniature runtime. If you came here for architecture candy, reactive state wizardry, and “how the hell is this form alive?” moments… yeah, this is that section.

The snippet below is the whole contract in motion. DNA drops in → hooks assemble defaults → steps self-build → validation slams the brakes if something’s off. It’s the closest thing to watching the UI read its own source code.

// src/app/vendors/.../DynamicServiceFormStep.tsx (simplified)
const { dnaConfig, loading, error, reload } = useDNALoader(serviceTypeId);
const {
  filteredBaseFields,
  buildInitialBaseFields,
  buildInitialPricingFields,
  buildInitialDocuments,
  buildInitialAvailability,
  shouldRenderPricing,
  shouldRenderLocation,
  getIgnoredLocationFields,
  validateAll,
} = useDnaFormConfig(dnaConfig);

useEffect(() => {
  if (!dnaConfig) return;
  setBaseFields((prev) => prev ?? buildInitialBaseFields());
  setPricingFields((prev) => prev ?? buildInitialPricingFields());
  setDocuments((prev) => prev ?? buildInitialDocuments());
  setAvailability((prev) => prev ?? buildInitialAvailability());
}, [
  dnaConfig,
  buildInitialBaseFields,
  buildInitialPricingFields,
  buildInitialDocuments,
  buildInitialAvailability,
]);

const steps = useMemo(() => {
  const base = ["details"];
  if (shouldRenderPricing) base.push("pricing");
  if (shouldRenderLocation) base.push("location");
  if (dnaConfig?.required_documents?.length) base.push("documents");
  if (dnaConfig?.availability_required) base.push("availability");
  return [...base, "review"];
}, [dnaConfig, shouldRenderPricing, shouldRenderLocation]);

const canProceed = validateAll({
  baseFields,
  pricingFields,
  locationFields,
  documents,
});

Those lines? That’s the blood circulation system of the entire form engine.

Inside the Engine Room (Where the Magic Lives)

1. Loading + Normalizing (a.k.a. “Nobody Enters Without DNA”)

useDNALoader is the bouncer. No serviceTypeId? → no entry. Backend hiccups? → take a seat, here’s the error.

It guarantees zero partial renders — the UI never stumbles into some half-loaded Frankenstein state.

// src/hooks/useDNALoader.ts
export function useDNALoader(serviceTypeId?: number) {
  const [dnaConfig, setDnaConfig] = useState<DNAConfig | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const load = useCallback(async () => {
    if (!serviceTypeId) return setDnaConfig(null);
    setLoading(true);
    try {
      const raw = await getDNALoader(serviceTypeId);
      setDnaConfig(raw.data);
    } catch (err) {
      setError(
        err instanceof Error ? err.message : "Failed to load DNA configuration",
      );
    } finally {
      setLoading(false);
    }
  }, [serviceTypeId]);

  useEffect(() => void load(), [load]);
  return { dnaConfig, loading, error, reload: load };
}

Once DNA arrives, useDnaFormConfig puts on the lab coat and starts preparing everything the UI will need — initial values, field filters, rules, cross-field validation policies, you name it.

const filteredBaseFields = useMemo(
  () =>
    dnaConfig?.base_fields.filter((f) => !LOCATION_FIELD_NAMES.has(f.name)) ??
    [],
  [dnaConfig],
);

We keep all builders inside this hook so when the backend introduces some new exotic field type (yes, it’ll happen), we update one place — the entire app becomes fluent instantly.

2. State Scaffolding & Wizard Navigation (The Self-Assembling UI)

DynamicServiceFormStep is the conductor. Every step of the wizard is dynamically generated from DNA, meaning the UI literally builds itself.

It feels handcrafted for every service type… but it’s all data-driven.

3. Leaf Field Rendering (Where Chaos Becomes UI)

DynamicField is the universal renderer. It’s the place where every weird option, placeholder, boolean, regex, integer, and multi-select transforms into a polished Shadcn component.

Highlights:

And yes — the renderer solves headless UI quirks like a champ.

4. Specialized Components for Specialized Pain

Some domains break all the rules, so we isolate them:

Each one keeps UX high-level while offloading the rules into useDnaFormConfig. One source of truth, zero surprises.

Validation & Serialization: The Guardrails That Never Sleep

Before the “Next” button does anything:

validateAll checks everything:

No step moves unless the data is clean.

Then, when the user submits:

buildServiceFormData serializes the entire payload using the same DNA definitions, so backend and frontend never drift.

form.append(field.name, value ? "true" : "false");

Arrays? Indexed keys. Files? Correct multipart names. Booleans? "true"/"false". Complex structures? Flattened but not mangled.

It’s deterministic, predictable, and very hard to break accidentally.

Guardrails, Battle Scars & Lessons Learned

What This Architecture Actually Proves

This system demonstrates:

And honestly? It shows we can take a chaotic requirement like:

“Make this form build itself from JSON… and make it look premium.”

…and deliver something robust enough to become the backbone of the vendor onboarding flow.

Conclusion: Why This Architecture Was Worth Every Late Night

At the end of the day, this whole DNA-driven system isn’t just a clever trick — it’s a survival strategy. When the business keeps evolving, when service types multiply like Pokémon, and when backend rules shift faster than product can finalize documentation, this architecture becomes the difference between scaling gracefully… and drowning in hard-coded forms.

By pushing all variability into DNA and letting the UI self-assemble, we built something that is:

And the best part?

Every time the backend ships a new configuration and the entire UI rearranges itself with zero code changes… you get that quiet moment of satisfaction that says:

Yep. This is why we engineered it properly. This is why the architecture matters.

The DNA engine isn’t just a form builder — it’s a foundation. One that will keep absorbing complexity so the product team can keep dreaming, the backend can keep iterating, and the UI will always stay in sync without ever falling apart.

💬 Join the Discussion

Share your thoughts and engage with the community

Loading comments...