7 Frontend Architecture Mistakes That Hurt Teams as Applications Grow
Most frontend architecture problems don't look like problems at first.
The application works. Features ship. The team moves quickly.
Then the product grows.
More developers join. Business logic becomes more complicated. Components become interconnected, and changes that once took hours start taking days because nobody is completely sure what else they might break.
That's when architecture starts to matter.
One thing production frontend work has taught me is that scalability isn't only about performance.
It's also about how easily a team can continue changing an application as it grows.
Here are seven mistakes that make that increasingly difficult.
1. Putting Too Much Business Logic Inside Components
Components often start simple and gradually become responsible for everything:
- Rendering UI
- Fetching data
- Validation
- Business rules
- Error handling
- Navigation
Consider a transfer form.
It might begin as a few inputs but eventually handle balance validation, transaction limits, fees, currency formatting, API calls, errors, and successful transaction behaviour.
Now that logic is tightly coupled to one interface.
A better separation might be:
useTransfer() → reusable behavior
transferService → API communication
TransferForm.vue → UI and user interaction
Not every function needs its own abstraction. The important question is whether the logic belongs specifically to that UI.
Takeaway: Components should primarily describe interfaces. Give reusable business logic somewhere else to live.
2. Making Everything Global State
Pinia makes global state easy.
That doesn't mean everything belongs there.
I've seen applications store everything globally—from authenticated users and wallet balances to modal visibility and temporary form values.
The result is a state layer that becomes increasingly difficult to understand.
Instead, ask:
Who actually needs this state?
If one component needs it, keep it local.
If a component tree needs it, keep it close to that tree.
If unrelated parts of the application genuinely need it, then global state makes sense.
const isModalOpen = ref(false)
probably doesn't need to live beside your authentication state.
Takeaway: Keep state as close as possible to whoever owns it.
3. Building Giant "Reusable" Components
Reusability can become a trap.
You start with a reusable table.
Then you add:
- Pagination
- Selection
- Editing
- Search
- Actions
- Custom headers
- Loading states
Eventually:
<AppTable :selectable :editable :paginated :searchable :show-actions ... />
You now have a component with 20 props, numerous events, and conditional behaviour everywhere.
Technically reusable.
Practically difficult to maintain.
Composition often scales better.
Instead of making one component understand every scenario, build smaller primitives that can work together:
Table
TableRow
Pagination
EmptyState
Takeaway: Don't measure reusability by how many scenarios one component supports. Measure it by how easily your abstractions can be combined.
4. Mixing Server State With UI State
Not all state represents the same kind of truth.
Consider a transaction page.
Server state:
- Wallet balance
- Transactions
- Beneficiaries
- Transfer limits
UI state:
- Selected tab
- Open modal
- Search query
- Current form step
Server state has concerns like fetching, caching, synchronisation, staleness, and refetching.
isConfirmationModalOpen doesn't.
Keeping these concerns separate makes state transitions much easier to reason about.
After a successful transfer, for example:
- The modal closes
- The form resets
- The wallet balance refreshes
Those are three different state transitions with different lifecycles.
Takeaway: Understand where your state comes from before deciding how to manage it.
5. Organising Code Without Considering How the Product Grows
Most Vue/Nuxt applications start with:
components/
composables/
stores/
services/
types/
That can work perfectly well.
But as applications grow, a single feature may become scattered across all five directories.
For larger products, grouping related code by domain can sometimes make navigation easier:
features/transfers/
features/wallets/
features/transactions/
Each feature can contain the components, composables, services, and types that belong to it, while genuinely shared code remains elsewhere.
There's no universally correct folder structure.
The question is:
Does your structure reflect how your team actually changes the product?
Takeaway: Optimize architecture for change, not for having the prettiest folder tree.
6. Abstracting Too Early
Frontend architecture can also become too sophisticated.
A simple request suddenly becomes:
Component → composable → repository → service → adapter → HTTP client.
For one endpoint.
Abstraction is valuable when it hides meaningful complexity or creates useful reuse.
Otherwise, you've simply replaced simple code with more code.
Before adding another layer, ask:
- Is this repeated?
- Does it create a meaningful boundary?
- Will it change independently?
- Does it make the calling code easier to understand?
If not, you may not need the abstraction yet.
Takeaway: Let complexity earn its abstractions.
7. Ignoring Developer Experience
Architecture isn't only about code.
It's about people changing code.
Warning signs include developers regularly asking:
"Where should this logic go?"
"Why do we have three ways of fetching data?"
"Which component am I supposed to use?"
Good architecture reduces these repeated decisions.
Teams benefit from shared conventions around:
- State ownership
- Component boundaries
- API communication
- Error handling
- Naming
- Testing
They don't need to be rigid rules.
But there should be a clear default.
Takeaway: Good architecture should reduce cognitive load for the team, not increase it.
Architecture Is About the Cost of Change
Almost any architecture works when an application is small.
The real test comes later.
What happens when five engineers need to work on related features?
What happens when a requirement contradicts an assumption made two years ago?
What happens when the engineer who originally built a feature leaves?
Good architecture doesn't remove complexity.
It puts complexity somewhere the team can understand and manage.
That's why I've gradually started thinking about frontend architecture less in terms of folder structures and design patterns and more in terms of one question:
How expensive will the next change be?
If every new feature makes the next feature harder to build, the architecture is working against the team.
If developers can understand ownership, make changes confidently, and extend the system without constantly breaking unrelated functionality, the architecture is doing its job.
Good frontend architecture makes change predictable.
Top comments (0)