There’s a component in your codebase that started innocent. It was just a product list. Today it fetches data from two endpoints, calculates the cart total, applies a coupon rule, decides whether the buy button should be disabled, and still picks which error message to show. Nobody planned this. It happened one reasonable pull request at a time, each with a good justification at the moment. By the time you notice, opening that file sends a chill down your spine, and any change comes with the fear of breaking three things that seemed unrelated.

That component is the symptom. The cause is older, and almost always the same: we let the UI library become the place where business logic lives.

Logic trapped in the component vs extracted to a domain module
Before: the component does everything. After: the UI only renders and delegates to a framework-free domain module.

What React (and Vue) Actually Deliver

React and Vue are libraries for building Views. They are excellent at this, and it’s worth remembering exactly what they give you: component rendering, local state management, user interaction handling, and UI lifecycle control. That’s a powerful list, and you can build entire products on top of it.

The problem appears in what’s not on the list. React and Vue don’t give you data transformation patterns, don’t resolve cross-cutting concerns like authentication and logging, don’t model your domain, and don’t handle data access. That’s not a criticism. A paintbrush isn’t obligated to compose the painting. The issue is that the framework lets you put all of this inside a component, and for a while it even feels productive. The bill comes later.

The Five Symptoms

When business logic leaks into components and hooks, the same five symptoms appear across every codebase, regardless of the stack. Business logic gets scattered, with no single place to look for a rule. Maintenance becomes more expensive as the app grows, because every change touches the UI. Testing requires mounting a component even when you just want to verify a calculation. Switching frameworks means rewriting business rules, because they were never separated from the framework. And new features keep breaking old ones, since unrelated logic shares the same file.

If you’ve worked on any frontend with more than two years of life, you recognize all five. It’s not a sign of a bad team. It’s the default outcome of treating a View library as if it were an architecture.

What a “Good” Architecture Actually Looks Like

It’s easy to talk about architecture in the abstract. It’s more useful to define it by what it lets you do. Your architecture is working when you can test business logic without rendering a single component, when you can swap React for Vue (or a CLI) without rewriting domain logic, when different teams work on different parts without stepping on each other, when a change in the data source (from REST to GraphQL, for example) doesn’t touch business rules, and when someone new to the team understands the code and contributes quickly.

Notice that none of these criteria mention a framework, a folder name, or a library. They all talk about one thing: independence. Of the UI from the logic, of the logic from the data source, and of teams from each other.

The CLI Test

There’s a quick test that captures almost all of that list. Ask yourself: could I run this feature from a command line, without any UI, reusing the exact same logic?

If the answer is yes, your rules live in a place the framework can’t see, which is exactly where they should be. If the answer is no, because the logic only exists inside a component or a hook, then your UI and your domain are fused, and all the costs from the previous section are already running. This isn’t about building a real CLI. It’s a mental experiment that exposes where your logic actually lives.

I use this as a code review question. When there’s doubt about where a piece of code should live, the CLI test usually answers before any discussion about layers.

How to Get Out of This Without Going Overboard

The way out isn’t a new library, it’s a habit: keep business logic in framework-agnostic modules — functions and classes that don’t import anything from React or Vue — and let the View layer handle only presentation. In practice, this means separating responsibilities. Presentation handles what the user sees. The domain holds the rules (can this product go in the cart? what’s the total after the coupon?). Data access knows whether you’re talking to REST or GraphQL and translates the raw response into something the domain understands.

In an e-commerce context with Vue/Nuxt, where I spend my days, the pressure point is always the cart and checkout. Price rules, coupon validation, and availability checks tend to leak into components and composables because, under deadline, that’s the shortest path. Pulling those rules into isolated modules pays twice: the same logic can serve a Nuxt page, a script, or a future native screen, and it becomes testable without spinning up the entire UI. If you use Vue, the post Composable Abstraction Layer: the missing pattern between Pinia and your Vue components shows exactly what this pattern looks like in practice with Pinia.

The warning in the other direction is worth making, because overengineering is as real a failure mode as the mess. You’re not supposed to fill the project with services and repositories because a book told you to. For an MVP, a simple CRUD screen, or something with a two-week deadline, putting logic where it’s convenient is usually the right call. The skill is giving the problem the amount of structure it asks for, and revisiting that decision as the product grows, rather than deciding once and defending it forever.

What I Would Do Today

If I could plant a single idea in a frontend team, it wouldn’t be “always add layers.” It would be the CLI test as a habit. Most teams don’t need a textbook architecture; they need a cheap, honest way to check whether domain logic is trapped inside components. From there, the decision of how much structure to add becomes much easier, because it stops being religion and becomes a response to a concrete problem.

If you want a practical next step, pick a component that scares you and apply the CLI test to it. List what’s inside that is presentation and what is business rule. That exercise alone usually reveals the first function that deserves to leave the component and become a domain module. In the next post, I take this idea and show how it becomes three concrete layers — Presentation, Domain, and Data — in a catalog with cart example.


If this made sense, I write about frontend architecture, technical leadership, and career. Find me on LinkedIn and GitHub.