Portfolio/Writing/Business logic belongs on the backend, and the front end stays dumb

Business logic belongs on the backend, and the front end stays dumb

The front end is not dumb, it is not authoritative. Rules that money, security or correctness depend on live on the server and are enforced there. The server sends decisions, the client renders them, and any client-side copy of a rule is optional decoration.

"Keep the front end dumb" is a good slogan and a slightly wrong one. The front end is full of logic: interaction, presentation, view models, optimistic updates. What it is not is authoritative. Any rule that money, security, or data correctness depends on lives on the server and is enforced there, no matter what the client sends. The client's copy of that rule, if it has one, is a convenience you could delete without breaking anything real.

What counts as business logic#

The rules that decide whether an action is allowed, what it costs, what state it produces, and what counts as valid:

  • Pricing, discounts, tax, currency conversion, fees.
  • Authorization: who can see or do what.
  • Workflow state transitions: draft to submitted to approved, and which moves are legal.
  • Quotas, rate limits, eligibility, entitlements, tier thresholds.
  • Inventory, availability, booking conflicts.
  • Data integrity: invariants that must hold across records.

Why it has to be on the server, and enforced there#

  • The client is untrusted. Anyone can open devtools, replay a request, or script the API directly. A client-side check is a UX affordance, never a gate.
  • There is more than one client. Web, mobile, partner integrations, internal tools, cron jobs. Logic in the web bundle is absent from all of them and drifts from any that copied it.
  • The server has the full picture. Other users' state, real inventory, the authoritative clock, rate-limit counters. The client sees a slice.
  • You cannot patch deployed clients on demand. A pricing bug shipped in a mobile app lives for weeks. The same bug on the server is a deploy.
  • One place to test and audit. A rule enforced in the endpoint has one test suite and one log. A rule scattered across three clients has none that agree.
If a malicious client skipped this check, would something bad happen: money moves wrong, data corrupts, access leaks, an invariant breaks? Then the server enforces it, and the client's version is optional decoration.

The front end is not dumb, it is not the judge#

Plenty of real logic still belongs in the client:

  • Presentation and formatting: locale, currency display, relative time, layout, truncation.
  • Interaction and flow: wizard steps, optimistic updates with rollback, focus, transitions, undo.
  • View models: mapping the API shape to what a component renders. The anti-corruption layer.
  • Input validation for immediate feedback. A mirror of a small subset of server rules, never the source.
  • Rendering permissions the server already decided: a capabilities or permissions object in the response tells the client which buttons to show. That is displaying a server decision, not making one.
  • Client routing, code splitting, and caching of server responses.

The pattern: the server sends decisions#

Do not send raw ingredients and let the client cook. Send the result, plus whatever breakdown the UI needs to explain it.

jsonc
// BAD: the client has to know the discount table and the tax rules
{
  "lineItems": [ { "sku": "A", "qty": 2, "unitPrice": 1000 } ],
  "customerTier": "gold",
  "couponCode": "SPRING"
}

// GOOD: the server computed it. The client renders the number.
{
  "lineItems": [ { "sku": "A", "qty": 2, "unitPrice": 1000, "lineTotal": 2000 } ],
  "subtotal": 2000,
  "discount": { "code": "SPRING", "amount": 300, "label": "Spring 15%" },
  "tax": { "amount": 145, "label": "VAT 8.5%" },
  "total": 1845,
  "currency": "EUR",
  "capabilities": { "canApplyCoupon": true, "canCheckout": true }
}

The client shows total, renders the discount and tax lines from their labels, and enables the checkout button because capabilities.canCheckout is true. It never adds the numbers up itself, and it never decides on its own whether checkout is allowed. When the user checks out, the server recomputes everything and rechecks the capability, because the request is still untrusted.

The duplication question#

Some rules do get mirrored on the client, for UX. Keep it disciplined:

RuleDecided byClient may mirror it?
Required field, max length, email formatBackend (authoritative)Yes, for instant feedback
Password policyBackendYes, show the checklist, still enforce on submit
Price, discount, tax, feesBackend onlyNo. Display the server’s numbers
Can this user perform this actionBackend onlyNo. Render the server’s capability flag
Is this workflow transition legalBackend onlyNo. Show/hide the action from a server-sent list
Quota or tier limit reachedBackend onlyDisplay the server’s "remaining" value, do not compute eligibility
If you mirror a rule, derive both from one source
A client "required field" check and a server one should come from the same schema (a shared Zod contract, or generated from one spec) so they cannot silently disagree. More on that in the API contracts post.

Anti-patterns#

  • The client computes the price from line items and a discount table, then POSTs the total, and the server trusts it.
  • if (user.role === "admin") gates a mutation, and only in the component.
  • Security-relevant feature flags evaluated client-side with no server check behind them.
  • Business rules in a utils/pricing.ts imported by components, with no server equivalent.
  • The front end "knows" the tax rates, the tier thresholds, or the referral bonus formula.
  • A workflow state machine reimplemented in the store that disagrees with the backend's.
  • "We will validate on the client to save a round trip" for something that has to be authoritative.

Where it genuinely blurs#

  • Local-first and offline apps. Logic has to run on the client because there may be no server for a while. The server reconciles later (CRDTs, a sync engine). Different architecture, and a deliberate one.
  • Pure client-side tools. A calculator, an image editor, a diagram tool with no backend. The logic is the product and there is nowhere to move it.
  • Latency-critical interactions. A game or a drawing surface predicts on the client and reconciles with the server, which stays authoritative.
  • Rich forms. Complex cross-field validation on the client is fine as UX. It is still not the gate.
Summary
The front end owns presentation, interaction, view models, and UX-level input feedback. It does not own pricing, authorization, workflow rules, quotas, or any decision that money or security depends on. The server computes those and enforces them on every request, and it sends the client decisions (a total, a capability flag, a list of allowed actions) rather than raw data to be judged. Any client-side copy of a real rule exists for feedback only, is derived from the same source as the server's, and could be deleted without weakening anything.

Next up
API contracts: what the backend owns, what the front end owns, and the schema in the middle

Data cleaning belongs on the backend. The front end shapes for presentation, not repair. Both sides validate the same contract from one schema, and the backend never omits keys or lies with optionality, because every gap becomes defensive code in every client.

Read next →