DEV Community

Adejuwon Oshadipe
Adejuwon Oshadipe

Posted on

How We Structured a Large Vue/Nuxt Application Without Losing Our Sanity

Every Vue application starts the same way.

A few pages.

A handful of components.

One or two API calls.

Everything feels simple.

Then the product grows.

New features are added.

More engineers join the team.

Business requirements evolve.

Before long, you're working with hundreds of components, dozens of API endpoints, multiple user roles, and features that all depend on one another.

At that point, the biggest challenge is no longer writing Vue.

It's managing complexity.

Over the last few years building production applications with Vue and Nuxt—particularly in fintech products—I've learned that architecture decisions made early can either accelerate a team or slow it down for years.

Here are some of the principles that have helped us keep growing applications maintainable.


The Framework Was Never the Problem

One lesson I've learned is that Vue rarely becomes the bottleneck.

Developers do.

Vue is intentionally flexible.

There are multiple ways to solve almost every problem.

That flexibility is one of its biggest strengths.

But without agreed conventions, every engineer naturally solves similar problems differently.

One page fetches data inside the component.

Another uses a composable.

A third talks directly to the store.

Everything works...

Until six months later when nobody remembers why things are different.

Scaling a Vue application isn't about choosing the perfect framework.

It's about reducing unnecessary decisions.


We Started Organising Around Features, Not File Types

Early in my career, I organised projects the way many tutorials teach.

Everything lived inside folders like:

  • Components
  • Views
  • Utils
  • Services

It worked.

Until the project became large.

Finding everything related to one feature meant jumping between five or six folders.

When working on financial products, we shifted our thinking.

Instead of organising code around what it was, we started organising it around what it did.

Instead of thinking:

"Where should this component live?"

We started asking:

"Which business feature owns this?"

That single mindset shift changed how we navigated the codebase.


A Real Example: Wallets, Transfers and Transactions

One of the products I worked on included several closely related financial features:

  • Wallet management
  • Money transfers
  • Transaction history

At first glance, they seem like one feature.

They're not.

Each has its own responsibilities.

The wallet is responsible for balances, funding methods and available accounts.

Transfers focus on initiating movement of funds, validation, recipients and confirmation.

Transactions are historical records, filtering, pagination and status tracking.

Early versions of the application had logic scattered across shared folders.

Wallet components referenced transfer utilities.

Transfer pages duplicated transaction formatting.

Several components were making similar API calls in slightly different ways.

Nothing was technically broken.

But the codebase was becoming harder to understand.

So we reorganised the application around business domains instead.

Each feature owned:

  • Its components
  • Its composables
  • Its API layer
  • Its types
  • Its tests

Instead of a developer searching the entire project to understand how transfers worked, almost everything related to transfers lived together.

The result wasn't fewer lines of code.

The result was less cognitive load.

New engineers could understand features much faster because the project reflected how the business actually worked.

That experience completely changed how I think about frontend architecture.


Components Should Render Interfaces

One mistake I've made—and reviewed many times—is allowing components to become responsible for everything.

Fetching data.

Formatting values.

Opening modals.

Handling permissions.

Managing API errors.

Rendering UI.

Eventually you end up with components hundreds of lines long.

Now I ask a much simpler question:

Is this component displaying information, or making business decisions?

If it's making business decisions, that logic probably belongs somewhere else.

Presentation becomes easier when business logic has a clear home.


Composables Became Our Building Blocks

Vue 3's Composition API changed how I think about reusable code.

Instead of copying logic between pages, we extracted repeated behaviour into composables.

Things like:

  • Pagination
  • Search
  • Currency formatting
  • Permissions
  • Data fetching
  • Infinite scrolling

But one lesson became very important.

A composable should solve one problem well.

Once it starts becoming responsible for unrelated concerns, you've simply created another large component—just in a different file.


We Became Much More Careful About Global State

One of the easiest mistakes to make in growing applications is putting everything into Pinia.

Just because multiple components can access state doesn't mean they should.

We started asking one question before adding anything to the global store:

Who actually needs this?

If the answer was:

"Only this page."

Then it stayed local.

Global state became reserved for information that genuinely belonged to the entire application:

  • Authenticated user
  • Permissions
  • Shared configuration
  • Common reference data

Everything else stayed close to where it was used.

That one rule significantly reduced unnecessary complexity.


Separate Business State From UI State

One subtle improvement that paid dividends was separating business data from interface state.

Take a transfer page.

Business state includes:

  • Amount
  • Currency
  • Recipient
  • Exchange rate
  • Transfer status

UI state includes:

  • Is the drawer open?
  • Which tab is selected?
  • Is the confirmation modal visible?
  • Is the filter expanded?

They're both reactive.

But they represent completely different concerns.

Keeping them separate made components easier to reason about and far easier to debug.


Consistency Beats Cleverness

I've stopped chasing "perfect architecture."

Instead, I value predictable architecture.

Developers don't need ten different ways to solve the same problem.

They need one good approach that everyone understands.

Consistency speeds up:

  • Onboarding
  • Code reviews
  • Debugging
  • Feature development

It's one of the highest-leverage investments a team can make.


Architecture Is Really About People

When we hear the word architecture, it's easy to think about diagrams, layers and patterns.

I've come to think about it differently.

Architecture is communication.

Good architecture helps another engineer understand your decisions without asking you.

It reduces guessing.

It reduces duplication.

It makes the next feature easier to build than the previous one.

That's the real goal.


Final Thoughts

One lesson has stayed with me throughout every large Vue project I've worked on.

Frameworks don't become messy.

Projects do.

And projects become messy because every small decision compounds over time.

Choosing where state belongs.

Deciding how components communicate.

Organising features around the business instead of the framework.

Separating concerns before they become problems.

None of these decisions feels significant on day one.

But two years later, they're often the difference between a codebase that enables engineers and one that slows them down.

For me, scalable frontend engineering isn't about writing more code.

It's about making sure the next engineer can understand, extend, and improve what you've already built with confidence.

Top comments (1)

Collapse
 
phongdesigns profile image
Phong Designs AI System

"Scaling a Vue application isn't about choosing the perfect framework. It's about reducing unnecessary decisions." That is the whole thing, and it gets stated plainly far less often than it should.

One layer worth adding, because it changes the shape of the problem: every convention you describe is human-readable, and it holds because people choose to follow it. Code review, onboarding, the shared understanding of where things live. That has worked for a long time.

It changes once an agent is writing in the repo. An agent does not break a convention, it extends it. When it reaches something the structure does not cover, it does not stop and ask which feature owns this. It creates a plausible-sounding place, sitting next to the real ones, reading as though someone decided it. Your own diagnosis still applies exactly: nothing was technically broken. It just happens several times faster than a team of humans could produce it.

Which is why "Who actually needs this?" before adding to Pinia is the right question and also a human question. The agent equivalent has to be a closed set that can fail: an enumerated list of valid feature domains, valid composables, and something that complains when a new one appears without going through it. Not for strictness. So that the new thing surfaces instead of quietly landing.

Otherwise "consistency beats cleverness" holds exactly as long as discipline does, and there is now a contributor in the repo with no discipline.

Wrote up the same failure mode from the design side, where the invented thing is a token name rather than a folder: dev.to/phongdesigns/claude-to-figm...