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
capabilitiesorpermissionsobject 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.
// 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:
| Rule | Decided by | Client may mirror it? |
|---|---|---|
| Required field, max length, email format | Backend (authoritative) | Yes, for instant feedback |
| Password policy | Backend | Yes, show the checklist, still enforce on submit |
| Price, discount, tax, fees | Backend only | No. Display the server’s numbers |
| Can this user perform this action | Backend only | No. Render the server’s capability flag |
| Is this workflow transition legal | Backend only | No. Show/hide the action from a server-sent list |
| Quota or tier limit reached | Backend only | Display the server’s "remaining" value, do not compute eligibility |
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.tsimported 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.