The first time we really felt the cost, we were rewriting a frontend. The backend was in good shape: solid domain logic, clear rules, well-tested. But starting a new UI meant rebuilding every validation check from scratch. Required fields. Minimum values. Conditional logic. We’d already encoded all of that on the backend. Now we were doing it again, in JavaScript.

That’s the obvious cost. The subtler one took longer to see.

When Rules Live in Two Places, They Drift

Over time, frontend validation and backend validation stop matching. Someone updates a rule on the backend. A minimum length changes. A field becomes conditionally required based on a new business condition. The frontend doesn’t know. Users start having confusing experiences: the form lets them submit something that the backend rejects, or the backend accepts something that the frontend flags as invalid.

This isn’t a discipline problem. It’s a design problem. Duplicated knowledge drifts.

The SRP Problem in Disguise

There’s also a quieter issue: the violation of the Single Responsibility Principle (SRP). The frontend starts accumulating logic it shouldn’t own. “If the payment method is check, the check number field is required.” That’s a domain rule, not a UI concern. When the frontend owns it, every future frontend has to rediscover and re-implement it.

That cost materializes. When a UI technology is replaced, and they do get replaced, you don’t just rewrite the screens. You risk rewriting or losing the business logic embedded in them.

The Moment That Made It Undeniable

Here’s a scenario that made this constraint concrete for me.

A user opens a sales order to make a change. The order is in a state that allows modifications. She gets pulled away: a meeting, a call. While she’s away, the order gets picked and shipped. It’s no longer modifiable. But the browser tab is still open, still showing the order as editable.

She comes back and submits.

If validation only happens on the frontend, and the frontend’s state is stale, that submit might go through when it absolutely shouldn’t. The backend is the only party with current knowledge of the system’s state. It has to be the authority.

Frontend validation is advisory. Backend validation is authoritative. Not just philosophically — practically.

What If the Backend Described Its Own Rules?

Once I accepted that the backend owns the rules, a question followed: then why are we also coding validation logic in the frontend at all?

The answer was user experience. Waiting for a round trip to find out that a field is required is a bad experience. Immediate feedback matters.

But there’s a middle path. What if the backend described its validation rules in a format the frontend could read and apply? Not the logic itself, just a description: this field is required, this one has a max length of 50, this one must match this pattern. The frontend reads that description and sets up its own validators from it.

{
  "fields": {
    "Invoice.Date": { "required": {} },
    "Invoice.ReferenceNumber": {
      "required": {},
      "limitedLength": { "max": 50 }
    },
    "Item.Code": {
      "pattern": { "regex": "^[A-Z0-9-]+$", "reason": "Uppercase letters, numbers, hyphen" }
    },
    "Invoice.TypeId": {
      "options": { "1": "Service", "2": "Freight" }
    }
  }
}

We call this a recipe. The backend produces it. The frontend consumes it.

Two Stages, Two Jobs

This separates validation into two distinct stages.

Draft — while the user fills out the form. The frontend runs validators built from the recipe. The feedback is immediate. It’s advisory: the user sees hints based on what the backend declared, but nothing is enforced at this stage.

Submit — when the form is sent. The backend runs its full validation: the same rules, plus context-aware checks that the frontend can’t know. Is the order still modifiable? Does the referenced account still exist? This is authoritative.

The user gets fast feedback during editing. The system stays correct at submission time. Both, without duplicating logic.

A Practical Side Effect

When a validation rule changes, we deploy it to the backend. The frontend picks it up on the next recipe load. No frontend deployment needed. No frontend code to change.

The business understood this quickly. In a system where the UI has been rewritten more than once, validation didn’t need to be re-implemented each time. The new frontend consumed the same recipes. The rules didn’t move.

What’s Coming in This Series

This is the first post in a four-part series on the recipe pattern.

Post 2 looks at the frontend: how to consume a recipe, wire up validators from JSON, and map server errors back to form fields after a failed submit.

Post 3 covers the backend: how to build the recipe, what rules it can express, and how FluentValidation became part of the picture. It wasn’t in the original design — that story is worth telling separately.

Post 4 is about testing: covering a pattern that spans two layers, and what the tests reveal about the design.


The simplicity of this boundary is what makes it durable. The frontend doesn’t need to understand the rules. It just needs to know how to follow them.

Leave a Reply

Trending

Discover more from Claudio Lassala's Blog

Subscribe now to keep reading and get access to the full archive.

Continue reading