👋Hi, I'm Waqas — a Software Architect and Technical Consultant specializing in .NET, Azure, microservices, and API-first system design..
I help companies build reliable, maintainable, and high-performance backend platforms that scale.
Large Vue 3 applications: composition API, state, testing, and team workflows.
December 14, 2025 · Waqas Ahmad
Read the article
Introduction
This guidance is relevant when the topic of this article applies to your system or design choices; it breaks down when constraints or context differ. I’ve applied it in real projects and refined the takeaways over time (as of 2026).
Vue apps that grow without structure, type safety, or tests become hard to maintain and refactor. This article is a full guide to Vue.js at enterprise scale: structure (feature-based), composables for reuse, Pinia for global state, TypeScript and tooling, testing (unit and e2e), and team workflows that keep the codebase maintainable. For architects and tech leads, adopting composables, Pinia, and tests from early on lets Vue 3 scale; the ones that thrived had clear conventions from the start.
System scale: Varies by context; the approach in this article applies to the scales and scenarios described in the body.
Team size: Typically small to medium teams; ownership and clarity matter more than headcount.
Time / budget pressure: Applicable under delivery pressure; I’ve used it in both greenfield and incremental refactors.
Technical constraints: .NET and related stack where relevant; constraints are noted in the article where they affect the approach.
Non-goals: This article does not optimize for every possible scenario; boundaries are stated where they matter.
What is Vue and what does “enterprise scale” mean?
Vue (Vue.js) is a progressive JavaScript framework for building user interfaces and single-page applications (SPAs). It focuses on the view layer: components, reactivity, and a simple template syntax. You can adopt Vue incrementally—start with a few components and scale up to a full SPA with routing and state management. Vue 3 introduced the Composition API (alongside the Options API), which lets you organise logic by concern (e.g. auth, orders) using composables—functions that use ref, reactive, computed, and watch—instead of spreading logic across data, methods, and lifecycle hooks. That makes reuse and testing easier as the app grows.
Enterprise scale means: large codebases (many features, many developers), long-lived apps that must stay maintainable, testable, and performant. At scale you need clear structure (feature-based folders, naming), shared state (e.g. Pinia), type safety (TypeScript), and automated tests (unit and e2e). Without structure, teams step on each other’s toes; without tests, refactors become risky; without types, API changes break the UI in subtle ways. This article explains how to get there with Vue 3: structure, composables, Pinia, routing, testing, and team workflows.
Vue at enterprise scale at a glance
Concept
What it is
Vue 3
Progressive framework for UIs and SPAs; Composition API + Options API; reactivity, components, single-file components (.vue).
Composition API
Organise logic by concern with composables (ref, reactive, computed, watch); better reuse and testing than spreading across options.
Composable
A function that uses Vue reactivity to encapsulate reusable logic (e.g. useAuth, useOrders); keep components thin.
Pinia
Recommended state store for Vue 3; typed stores, no mutations (change state in actions); use for global shared state.
Feature-based structure
Folders by domain (auth, orders, profile); shared components and composables in a common folder; avoid deep nesting.
TypeScript
Type safety for props, events, API responses; strict mode; generate types from OpenAPI when possible.
Unit tests
Test composables and components in isolation (e.g. Vitest); mock API and store.
E2e tests
Test key user flows in a browser (Cypress, Playwright); run in CI after build.
Loading diagram…
Project structure
Use a feature-based (or domain-based) folder structure so that everything related to a flow (e.g. auth, orders, profile) lives together. Keep shared components and composables in a common folder; avoid deep nesting that makes imports long and refactors painful. Barrel files (index.ts) can help discoverability but use them sparingly so that tree-shaking stays effective.
What we’re building: A layout where each feature owns its views, components, and composables; shared code lives in shared/ so that multiple features can reuse it without circular dependencies.
What this is: Top-level features/ and shared/. Each feature has components, composables, and views. Shared holds cross-cutting UI, composables, types, and API client.
Step 2: Where things live
Item
Where it lives
Feature-specific view
features/<feature>/views/
Feature-specific component
features/<feature>/components/
Feature-specific composable
features/<feature>/composables/
Shared component
shared/components/
Shared composable
shared/composables/
API client and types
shared/api/, shared/types/
Router config
router/ (or src/router/)
How this fits together: Views import components and composables from their feature or from shared/. Composables call the API and optionally use Pinia. The router lazy-loads feature views so the initial bundle stays small. This structure keeps teams from stepping on each other and makes it clear where to add new code.
Composables: the unit of reuse
Composables are the main unit of reuse in Vue 3: extract logic (e.g. useAuth, useOrders) so that components stay thin and testable. A composable is a function that uses Vue reactivity (ref, reactive, computed, watch) and returns what the component needs. Keep composables focused (one concern) and typed (TypeScript) so that refactors are safe.
Why we do this: Putting all logic inside components makes testing hard and reuse impossible. By moving logic into composables, we can unit test without mounting components and reuse the same logic across multiple views.
What this file is: A composable that holds list, loading, error, and a derived pending list; it exposes fetchOrders so the component can trigger the load. The component does not hold the data—the composable does, so we can test the logic in isolation.
What this does: The view only orchestrates: it calls useOrders(), binds template to list, loading, error, and calls fetchOrders on click. All logic lives in the composable.
How this fits together: The composable owns the state and the API call; the component is a thin layer that renders and delegates. When you need the same orders logic elsewhere (e.g. a dashboard widget), you reuse useOrders() without duplicating code. Unit tests target the composable with a mocked ordersApi; the component can be tested with a real or mocked composable.
State management with Pinia
For global state shared across many components (e.g. current user, cart, app-wide settings), Pinia is the recommended store for Vue 3. Define stores by domain (e.g. authStore, ordersStore) and keep them small and typed. Use getters for derived state and actions for side effects (API calls, persistence). Avoid one giant store; split by feature so that teams can work in parallel and tree-shaking can drop unused stores.
When to use Pinia vs composable: Use Pinia when state must be shared across many components or persisted (e.g. user session, cart). Use a composable when state is scoped to one view or a small subtree (e.g. form draft, modal open/close).
What this file is: A Pinia store with state (list, loading), a getter (pending), and an action (fetchOrders). Components use useOrdersStore() to access the same state everywhere.
How this fits together: Any component that calls useOrdersStore() shares the same list and loading. When one component calls fetchOrders(), all components that use the store see the updated data. Use Pinia for cross-cutting state; use composables for feature-local state.
Routing and lazy loading
Use Vue Router for client-side routes. Lazy-load route components so that the initial bundle only includes the first screen; other views load on demand when the user navigates. That keeps First Contentful Paint and Time to Interactive low.
What this does: Each component is a function that returns a dynamic import(). The build splits these into separate chunks; the browser loads them when the user visits the route. Keep the default route (e.g. home) and the first few critical routes in mind when ordering; everything else can be lazy.
Testing: unit and e2e
Unit tests for composables and critical components: test logic in isolation with mocked API and store. E2e tests for key user flows: load the app in a browser, click and type, assert on the DOM or network. Run both in CI so that PRs cannot merge with failures.
What this does: We mock ordersApi.getOrders and assert that after fetchOrders(), list or error is updated correctly. No component is mounted—we test the composable alone.
E2e (Cypress/Playwright): Write tests that open the app, navigate to a route, perform actions (click, type), and assert on the DOM or API. Run e2e in CI after build; keep e2e for critical paths (e.g. login, checkout) and use unit tests for composables and components so that the suite stays fast and stable.
TypeScript and tooling
TypeScript gives type safety so that refactors and API changes are caught at compile time. Define interfaces for props, events, and API responses; use strict mode. Generate types from OpenAPI when the backend has a spec so that the front end and backend stay in sync.
Linting (ESLint with Vue and TypeScript rules) and formatting (Prettier) keep the codebase consistent. Run them in CI so that PRs cannot merge with style violations. Use vue-tsc --noEmit in CI to ensure types check without emitting build output.
What this does: Order and getOrders are typed so that composables and components get autocomplete and compile-time errors when the API shape changes. When the backend exposes OpenAPI, generate Order and other DTOs from the spec to avoid drift.
Team workflows and conventions
Conventions reduce friction when many developers work on the same app. Agree on: naming (e.g. composables useX, stores useXStore), file placement (feature vs shared), when to use Pinia vs composable (shared vs local state), and testing (unit for composables and critical components, e2e for key flows). Use PR reviews to reinforce structure and tests; run CI (lint, type-check, unit, e2e) so that broken code cannot merge. Document the structure and conventions in a short README or ADR so that new hires and contributors know where to put code and how to test it.
Best practices and common issues
Do: Use feature-based structure and composables for reuse; Pinia for global state; TypeScript and strict mode; unit tests for composables and e2e for critical flows; lazy-loaded routes to keep the initial bundle small; ESLint and Prettier in CI.
Don’t: Put all logic in one component (monolithic components); store everything in one giant store or duplicate state (state sprawl); skip tests or TypeScript; import entire libraries when you need one function (bundle bloat); use any or disable strict mode.
Common issues:
Monolithic components: Hard to test and reuse. Fix: Extract logic into composables; keep components thin (template + orchestration).
State sprawl: Duplicated or unclear ownership of state. Fix: Use Pinia for shared state; use composables or local state for scoped state; avoid prop-drilling—use provide/inject or a store when many components need the same data.
Missing tests: Refactors become risky. Fix: Unit test composables and critical components; e2e test key user flows; run tests in CI.
Type safety neglected:any or no types hide bugs. Fix: Define interfaces for props, events, and API responses; generate types from OpenAPI when possible.
Bundle size growth: Large initial load. Fix: Lazy-load routes; tree-shake; analyse bundle (e.g. vite-bundle-visualizer) and remove or replace heavy dependencies.
Summary
Vue 3 at enterprise scale means feature-based structure, composables for reuse, Pinia for global state, TypeScript, and tests from the start—so the codebase stays maintainable as it grows. Deferring structure or tests until “we need it” leads to refactor pain and sprawl; investing early keeps components thin and logic testable. Next, adopt one convention: feature-based folders, or composables for one domain, or Pinia for shared state; add TypeScript strict mode and run lint/type-check in CI, then add unit tests for composables and e2e for key flows.
Vue 3 and the Composition API scale when you use feature-based structure, composables for reuse, and Pinia for global state.
Keep components thin and logic in composables so that you can unit test and reuse across views.
Use TypeScript and strict mode so that refactors and API changes are caught at compile time.
Unit test composables and critical components; e2e test key user flows; run lint and type-check in CI.
Lazy-load routes and tree-shake to keep the initial bundle small; avoid monolithic components and state sprawl.
Invest in structure and testing early so that the codebase scales with the team.
Position & Rationale
I structure large Vue 3 apps with feature-based folders, composables as the unit of reuse, and Pinia for global state so components stay thin and testable. I adopt TypeScript and strict mode from the start so refactors and API changes are caught at compile time. I unit test composables and critical components and e2e test key flows; I run lint and type-check in CI so the codebase stays maintainable as it grows. I lazy-load routes and tree-shake to keep the initial bundle small; I avoid monolithic components and state sprawl. I reject skipping structure or tests “until we scale”—the cost of retrofitting is higher. I prefer composables over mixins or giant stores so logic is reusable and testable in isolation.
Trade-Offs & Failure Modes
What this sacrifices: Extra setup (TypeScript, Pinia, test harness) and conventions; flexibility is lower than a tiny prototype.
Where it degrades: When components grow into god components or state is scattered; when tests are skipped and refactors become risky; when the bundle grows and no one runs the analyser.
How it fails when misapplied: Using Vue at “enterprise scale” without structure or types—technical debt compounds. Adding Pinia everywhere instead of local state; or no global state when many features share it. Skipping lazy loading so the initial load is huge.
Early warning signs: “We’re scared to refactor”; “the main bundle is 2MB”; “we don’t know where this state lives”; “tests are flaky or missing.”
What Most Guides Miss
Guides often show Composition API and Pinia in isolation and skip when to use global state vs local and how to test composables. In practice, not everything belongs in Pinia; local state and props keep features decoupled. Testing composables (Vitest or Jest) with mocked dependencies is the way to keep logic safe to refactor—many teams only do e2e and miss fast feedback. Bundle analysis (e.g. vite-bundle-visualizer) and lazy loading discipline are underplayed; without them the app gets slow as it grows. Team workflows (conventions, PR rules, who owns structure) are rarely covered; they matter as much as the tech choices.
Decision Framework
If you’re building a large Vue 3 app → Use feature-based structure, composables for reuse, Pinia for shared state; add TypeScript and strict mode early.
If you need testability → Unit test composables and critical components; e2e test key flows; run tests and type-check in CI.
If the bundle is growing → Lazy-load routes; tree-shake; run bundle analyser and remove or replace heavy deps.
If state is confusing → Prefer local state and props where possible; use Pinia for cross-feature or cross-route state; avoid storing everything globally.
If the team is scaling → Document conventions (structure, naming, when to add a composable); run lint and types in CI; invest in onboarding.
Key Takeaways
Vue 3 at scale: feature-based structure, composables for reuse, Pinia for global state; TypeScript and tests from the start.
Keep components thin and logic in composables; lazy-load routes and control bundle size.
Revisit structure and tests when the team or codebase grows; don’t defer “until we need it.”
When I Would Use This Again — and When I Wouldn’t
I would use this Vue-at-scale approach again for any large or long-lived Vue 3 front end—structure, composables, Pinia, TypeScript, and tests from early on. I’d use it when the team is committed to Vue and we expect the app to grow. I wouldn’t use it for a one-off landing page or a tiny internal tool where the overhead isn’t justified. I wouldn’t skip structure or tests to ship faster; the payback is negative once the codebase grows. If the team is already on Angular or React and the product is stable, I wouldn’t switch to Vue for “enterprise scale” alone; the comparison (Vue vs Angular vs React) should drive the initial choice, then apply scale patterns to the chosen stack.
Frequently Asked Questions
Frequently Asked Questions
How do I structure a large Vue 3 application?
Use feature-based or domain-based folders (e.g. auth, orders, profile); keep shared components and composables in a common folder (e.g. shared/). Use composables for reuse and Pinia for global state. Keep components thin and logic in composables so that testing and refactoring stay easy.
When should I use Pinia vs local state?
Use Pinia (or similar) when state is shared across many components or needs to persist (e.g. user session, cart). Use local state (ref, reactive in a component or composable) when state is scoped to one component or a small subtree. Avoid storing everything in the store; reserve the store for truly global or cross-feature state.
How do I keep Vue components testable?
Extract logic into composables so that you can unit test without mounting components. Inject dependencies (e.g. API client) so that you can mock them in tests. Keep components thin (template + orchestration); test user flows with e2e (Cypress, Playwright).
What is the role of TypeScript in enterprise Vue apps?
TypeScript gives type safety so that refactors and API changes are caught at compile time. Define interfaces for props, events, and API responses; use strict mode. Generate types from OpenAPI when the backend has a spec so that the front end and backend stay in sync.
How do I avoid bundle size bloat in Vue?
Lazy-load routes (component: () => import(...) in Vue Router) so that each view is in a separate chunk. Tree-shake: avoid importing entire libraries when you need one function. Use a bundle analyser (e.g. vite-bundle-visualizer) to find large dependencies; replace or split as needed.
What are common mistakes when scaling Vue applications?
Monolithic components (all logic in one component), state sprawl (one giant store or duplicated state), missing tests, and ignoring TypeScript. Adopt structure early (feature-based folders, composables), test and type from day one, and run lint and CI so that the codebase scales with the team.
What is a SPA and why does Vue fit it?
A single-page application (SPA) loads one HTML page and updates the UI via JavaScript (no full page reloads). Vue fits SPAs because it is reactive (data drives the view), component-based, and lightweight. Use Vue Router for client-side routes and Pinia (or similar) for shared state so that the app feels fast and stays maintainable.
How do I organise composables vs components?
Composables hold logic (state, API calls, side effects); components hold template and orchestration (call composables, pass props, emit events). Put composables in features/<feature>/composables/ or shared/composables/; keep components in components/ or views/. One composable per concern (e.g. useAuth, useOrders).
Should I use Options API or Composition API in Vue 3?
For new and large apps, prefer Composition API: better reuse (composables), clearer logic organisation, and easier testing. Options API is still supported and fine for small components or when the team is more familiar with it. You can mix both in the same app if needed.
How do I run e2e tests for a Vue app?
Use Cypress or Playwright: write tests that load the app in a browser, click and type, and assert on the DOM or network. Run e2e in CI after build; use environments (e.g. staging) or static build so that tests run against a real deployment. Keep e2e for critical user flows; use unit tests for composables and components so that the suite stays fast.
What is Pinia vs Vuex?
Pinia is the recommended store for Vue 3: simpler API, TypeScript support, and no mutations (you change state in actions). Vuex is still supported but Pinia is the default for new projects. Migrate from Vuex to Pinia when you are ready; the concepts (state, getters, actions) are similar.
What is Vue?
Vue is a progressive JavaScript framework for building user interfaces and SPAs. Vue 3 offers the Composition API (composables, ref, reactive) for organising logic by concern and reusing it across components, alongside the Options API for developers who prefer it.
What is the Composition API?
The Composition API is a Vue 3 feature that lets you organise logic using composables (ref, reactive, computed, watch) instead of the Options API (data, methods, lifecycle hooks). It improves reuse and testing in large apps because logic is grouped by concern and can be shared across components.
What is a composable?
A composable is a function that uses Vue reactivity (ref, reactive, computed, watch) to encapsulate reusable logic (e.g. useAuth, useOrders). Extract logic from components into composables so that components stay thin and logic is testable in isolation.
How do I add TypeScript to a Vue 3 project?
Use Vue 3 with TypeScript: create tsconfig.json with strict mode, use .vue files with <script setup lang="ts">, and define interfaces for props, events, and API responses. Use defineComponent or <script setup> and type your ref and reactive state. Generate types from OpenAPI when the backend has a spec.
Related Guides & Resources
Explore the matching guide, related services, and more articles.