<v/>
{ }
Vue 3 · Architektur · Feature-Slicing · Composables
Dateibasierte Architektur für größere Vue-Projekte
von Feature-Slices bis zu sauberen Modul-Grenzen

Wer Vue-Projekte ohne durchdachte Dateistruktur aufbaut, kämpft spätestens beim zweiten Entwickler mit unklaren Verantwortlichkeiten und zirkulären Abhängigkeiten. Feature-basierte Vue-Architektur mit klar definierten Schichten und Composables als Herzstück löst dieses Problem strukturell, bevor es den Alltag verlangsamt.

15 Min. Lesezeit Feature-Slices · Composables · Pinia · Vue Router · Modul-Grenzen Vue 3 · Vite · TypeScript

1. Warum klassische Ordnerstruktur in Vue scheitert

Die klassische Vue-Projektstruktur mit components/, views/, store/, services/ und utils/ als Top-Level-Ordner funktioniert gut für kleine Anwendungen mit einem Entwickler und einem überschaubaren Scope. Sobald das Projekt wächst, werden diese Ordner zu Schubladen, in die alles hineingestopft wird. Eine einfache Feature-Anforderung führt dann dazu, dass Dateien in vier verschiedenen Ordnern angelegt oder geändert werden müssen — ein klares Zeichen, dass die **Vue-Architektur** nicht nach Zuständigkeit, sondern nach technischem Typ organisiert ist.

Das konkrete Problem: components/ enthält nach einem Jahr dreißig bis fünfzig Komponenten ohne erkennbare Beziehungen zueinander. Ein neuer Entwickler kann nicht erkennen, welche Komponente zu welchem Feature gehört, welche shared ist und welche nur von einer einzigen View verwendet wird. Gleichzeitig wächst der Pinia-Store zu einem monolithischen Objekt, das Zustand für vollkommen unverwandte Features hält. Die dateibasierte **Vue-Architektur** löst dieses Problem durch einen anderen Ausgangspunkt: Nicht der technische Typ, sondern die fachliche Zugehörigkeit bestimmt die Ordnerstruktur.

2. Feature-Slicing: der Kern dateibasierter Vue-Architektur

Feature-Slicing bedeutet, dass jedes fachliche Feature einen eigenen Ordner unter src/features/ erhält. Ein Feature ist dabei alles, was eine abgeschlossene Domäne abbildet: features/auth/, features/catalog/, features/checkout/, features/user-profile/. Innerhalb dieses Feature-Ordners liegen alle zugehörigen Komponenten, Composables, Stores, API-Calls und Typen. Das Ergebnis ist eine **Vue-Architektur**, in der das Löschen oder Refactoring eines Features mit minimalen Seiteneffekten verbunden ist, weil alle relevanten Dateien an einem Ort liegen.

Die Abgrenzung zwischen Features folgt dem Prinzip der schwachen Kopplung: Ein Feature darf nicht direkt auf den internen Code eines anderen Features zugreifen. Kommunikation zwischen Features läuft entweder über die shared/-Schicht, über Event-Busse oder über einen gemeinsamen Pinia-Store, der explizit als geteilt markiert ist. Diese Regel, konsequent eingehalten, verhindert das Entstehen von zirkulären Abhängigkeiten — dem häufigsten Wartungsproblem in gewachsenen Vue-Projekten ohne durchdachte **Vue-Architektur**.


// Recommended directory structure for scalable Vue 3 projects
src/
├── features/
│   ├── auth/
│   │   ├── components/       // LoginForm.vue, AuthGuard.vue
│   │   ├── composables/      // useAuth.ts, usePermissions.ts
│   │   ├── stores/           // authStore.ts
│   │   ├── api/              // authApi.ts
│   │   ├── types/            // auth.types.ts
│   │   └── index.ts          // public API of the feature
│   ├── catalog/
│   │   ├── components/
│   │   ├── composables/
│   │   ├── stores/
│   │   ├── api/
│   │   └── index.ts
│   └── checkout/
│       └── ...
├── shared/
│   ├── components/           // Button, Modal, Input — truly reusable
│   ├── composables/          // useDebounce, usePagination
│   ├── utils/                // formatDate, slugify
│   └── types/                // global.types.ts
├── app/
│   ├── router/               // route definitions, lazy imports
│   ├── stores/               // cross-feature stores only
│   └── App.vue
└── main.ts

3. Schichten innerhalb eines Feature-Slice

Innerhalb jedes Feature-Ordners gibt es eine klare Schichtenarchitektur, die von außen nach innen arbeitet. Die äußerste Schicht sind die Vue-Komponenten in components/ — sie sind rein präsentational und enthalten so wenig Logik wie möglich. Komponenten rufen Composables auf, lesen reaktive Daten daraus und geben Benutzereingaben weiter. Diese Schicht ist leicht testbar mit Testing Library, weil sie keine direkte Abhängigkeit auf API-Calls oder globale Zustände hat. Die **Vue-Architektur** wird durch diese Trennung stabiler gegen Refactorings in der Datenschicht.

Die mittlere Schicht bilden die Composables: Sie halten den lokalen Zustand, koordinieren API-Calls über die API-Schicht und stellen dem Template reaktive Daten zur Verfügung. Composables sind die richtige Stelle für Geschäftslogik, die weder in die Komponente noch in den globalen Store gehört. Die innerste Schicht ist die API-Schicht in api/: reine Funktionen, die HTTP-Requests auslösen und typisierte Antworten zurückgeben. Sie wissen nichts von Vue-Reaktivität und sind mit regulären Unit-Tests einfach testbar. Die index.ts jedes Features exportiert ausschließlich das, was andere Features oder der Router brauchen — die interne Struktur bleibt privat.

4. Composables als Herzstück der Geschäftslogik

Composables sind das mächtigste Werkzeug der **Vue-Architektur** in Vue 3. Sie ersetzen Mixins, die in Vue 2 die Hauptmethode zur Logik-Wiederverwendung waren, und lösen deren Probleme: keine impliziten Namenskonflikte, keine unklare Herkunft reaktiver Daten, volle TypeScript-Unterstützung. Ein gut geschriebenes Composable ist eine Funktion, die ref, computed und Vue-Lifecycle-Hooks kapselt und ein klar definiertes Interface nach außen gibt. Der Name beginnt per Konvention immer mit use, was die Zugehörigkeit zur Composables-Schicht sofort erkennbar macht.

Die wichtigste Design-Entscheidung bei Composables ist, ob sie ihren eigenen Zustand halten oder Zustand als Parameter erhalten. Ein Composable, das intern ref() aufruft, erstellt bei jedem Aufruf eine neue Zustandsinstanz — gut für Formular-Logik, die pro Komponenten-Instanz isoliert sein soll. Ein Composable, das einen Pinia-Store importiert, teilt seinen Zustand implizit mit allen Aufruferinnen. In der **Vue-Architektur** ist es wichtig, diese beiden Patterns nicht zu vermischen: Composables mit lokalem Zustand für Feature-interne Logik, Store-gestützte Composables für Feature-übergreifende Daten.


// features/catalog/composables/useProductList.ts
// Composable with local state — each caller gets its own instance
import { ref, computed, onMounted } from 'vue'
import { fetchProducts } from '../api/catalogApi'
import type { Product, ProductFilter } from '../types/catalog.types'

export function useProductList(initialFilter: ProductFilter = {}) {
  // Local reactive state — not shared between callers
  const products = ref<Product[]>([])
  const isLoading = ref(false)
  const error = ref<string | null>(null)
  const filter = ref<ProductFilter>(initialFilter)

  // Derived state — automatically updates when products or filter changes
  const filteredCount = computed(() => products.value.length)

  async function loadProducts() {
    isLoading.value = true
    error.value = null
    try {
      products.value = await fetchProducts(filter.value)
    } catch (e) {
      error.value = e instanceof Error ? e.message : 'Unknown error'
    } finally {
      isLoading.value = false
    }
  }

  function updateFilter(newFilter: Partial<ProductFilter>) {
    filter.value = { ...filter.value, ...newFilter }
    loadProducts()
  }

  // Lifecycle hook inside composable — no need to call in component
  onMounted(loadProducts)

  return { products, isLoading, error, filteredCount, updateFilter }
}

5. Shared-Layer: was wirklich geteilt werden darf

Die shared/-Schicht ist die gefährlichste Zone in jeder **Vue-Architektur**, weil sie zur Müllhalde für alles werden kann, das ein Entwickler nicht einem konkreten Feature zuordnen will. Die Regel für den Shared-Layer muss klar formuliert sein: Eine Datei gehört in shared/ nur dann, wenn sie von mindestens drei unabhängigen Features genutzt wird und keine Feature-spezifische Logik enthält. Ein Button, ein Modal, ein Pagination-Composable und ein Datums-Formatter gehören dort hin. Ein API-Call, der zufällig von zwei Features genutzt wird, gehört dagegen ins Feature, das die primäre Verantwortung für diese Daten trägt.

Shared-Komponenten folgen dem Prinzip der maximalen Generizität: Sie nehmen alle variablen Inhalte über Props und Slots entgegen und haben keine Meinung über die Domäne. Ein shared/components/DataTable.vue weiß nicht, ob er Produkte oder Bestellungen anzeigt — das gibt der Aufrufer über typisierte Props vor. Diese Abstraktion zahlt sich aus, wenn sich das Rendering-Verhalten an einer zentralen Stelle anpassen lässt, ohne alle Feature-Komponenten zu ändern. Die **Vue-Architektur** gewinnt dadurch an Konsistenz und reduziert doppelten Code, ohne die Feature-Isolation zu gefährden.

6. Routing und Lazy-Loading pro Feature

In einer dateibasierten **Vue-Architektur** definiert jedes Feature seine eigenen Routen und registriert sie beim zentralen Router. Das klassische Pattern ist eine routes.ts-Datei im Feature-Ordner, die ein Array von RouteRecordRaw-Objekten exportiert. Der zentrale Router in app/router/index.ts importiert diese Arrays und fügt sie zusammen. Das Ergebnis ist ein Router-Setup, das bei neuen Features nur eine Import-Zeile benötigt, ohne die bestehende Router-Konfiguration grundlegend anfassen zu müssen.

Lazy-Loading ist bei diesem Ansatz einfach und konsequent umzusetzen: Jede View-Komponente wird mit einer dynamischen Import-Funktion geladen, sodass der Vite-Bundler für jedes Feature automatisch einen separaten Chunk erstellt. Der initiale Bundle wird dadurch deutlich kleiner, weil nur der Code geladen wird, der für die aktuelle Route tatsächlich benötigt wird. In der **Vue-Architektur** bedeutet das: wächst ein Feature durch neue Unterseiten, wächst nur der Chunk dieses Features — der Rest der Anwendung bleibt unverändert. Route-Guards, die auf die Feature-eigene Authentifizierungslogik zugreifen, werden ebenfalls in der Feature-eigenen routes.ts definiert.

7. Pinia-Stores sauber organisieren

Pinia ist der offizielle State-Manager für Vue 3 und fügt sich natürlich in die Feature-Slicing-**Vue-Architektur** ein. Die Grundregel: Jeder Feature-Store gehört in features/[name]/stores/ und enthält nur den Zustand, der diesem Feature gehört. Store-Definitionen mit der Composition-API-Syntax von Pinia sind dabei zu bevorzugen, weil sie nahtlos mit Composables zusammenarbeiten und TypeScript-Typen ohne zusätzliche Wrapper-Typen durchreichen.

Stores, die Feature-übergreifend geteilt werden müssen — wie ein User-Store oder ein Notification-Store — gehören in app/stores/ und werden explizit als global markiert. Feature-Stores dürfen globale Stores importieren, aber globale Stores dürfen keine Feature-Stores importieren: Diese Einbahnstraße verhindert zirkuläre Abhängigkeiten in der **Vue-Architektur** auf Store-Ebene. Wenn ein Feature-Store zu groß wird und mehrere unverwandte Zustandsbereiche verwaltet, ist das ein klares Signal, das Feature weiter aufzuteilen oder Composables zur Zustandskapselung einzusetzen.


// features/auth/stores/authStore.ts
// Pinia store using Composition API syntax — type-safe and composable-friendly
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import type { User } from '../types/auth.types'
import { loginRequest, logoutRequest } from '../api/authApi'

export const useAuthStore = defineStore('auth', () => {
  // State
  const user = ref<User | null>(null)
  const token = ref<string | null>(localStorage.getItem('auth_token'))

  // Getters
  const isAuthenticated = computed(() => token.value !== null && user.value !== null)
  const userDisplayName = computed(() => user.value?.name ?? 'Guest')

  // Actions
  async function login(email: string, password: string) {
    const response = await loginRequest({ email, password })
    token.value = response.token
    user.value = response.user
    localStorage.setItem('auth_token', response.token)
  }

  async function logout() {
    await logoutRequest()
    token.value = null
    user.value = null
    localStorage.removeItem('auth_token')
  }

  return { user, token, isAuthenticated, userDisplayName, login, logout }
})

8. Modul-Grenzen durchsetzen und Abhängigkeiten prüfen

Die beste **Vue-Architektur** nützt wenig, wenn niemand die definierten Grenzen durchsetzt. In kleinen Teams reicht oft eine dokumentierte Konvention, aber ab vier oder mehr Entwicklern empfiehlt sich ein automatisiertes Werkzeug. ESLint mit dem Plugin eslint-plugin-import kann durch entsprechende Konfiguration verhindern, dass Feature-A direkt in die internen Komponenten von Feature-B importiert. Die Regel: Immer nur über index.ts importieren, nie aus Unterpfaden anderer Features. Ein Import wie import X from '@/features/auth/components/LoginForm.vue' von einem anderen Feature aus ist eine Grenzverletzung.

Zusätzlich lohnt es sich, madge oder dependency-cruiser in die CI-Pipeline zu integrieren. Diese Werkzeuge visualisieren und validieren den Dependency-Graphen des Projekts. Eine Konfiguration, die zirkuläre Abhängigkeiten und verbotene Cross-Feature-Imports als Fehler markiert, verhindert, dass die **Vue-Architektur** über die Zeit erodiert. Im Pull-Request sieht man sofort, wenn ein neuer Feature-Import gegen die definierten Modul-Grenzen verstößt, und kann gegensteuern, bevor sich das Muster im gesamten Codebase ausbreitet.

9. Architektur-Ansätze im Vergleich

Es gibt mehrere bekannte Ansätze zur Strukturierung von Vue-Projekten. Die richtige Wahl hängt von Teamgröße, Feature-Anzahl und Langzeitperspektive ab.

Ansatz Struktur Skaliert bis Hauptproblem
Typ-basiert components/, views/, store/ 1–2 Entwickler, <10 Features Unklare Zugehörigkeit bei Wachstum
Feature-Slicing features/[name]/… Team mit 5–15 Personen Disziplin bei Shared-Layer nötig
Monorepo (Nx/Turborepo) Separate Packages pro Feature Enterprise, viele Teams Hoher Setup- und Tooling-Aufwand
Nuxt-Module Nuxt-Konventionen + Module SSR-Projekte mit Nuxt Nur mit Nuxt sinnvoll einsetzbar
DDD-Schichten Domain, Application, Infrastructure Komplexe Domänen-Logik Overhead für kleine Teams

Für die meisten Vue-Projekte, die über die Prototyp-Phase hinauswachsen, ist Feature-Slicing der pragmatischste Ansatz. Er bringt die Vorteile einer klaren **Vue-Architektur** ohne den Overhead eines vollständigen Monorepo-Setups. Die Grenze zum Monorepo-Ansatz ist sinnvoll, wenn Features von verschiedenen Teams mit unterschiedlichen Deployment-Zyklen entwickelt werden — dann macht die physische Pakettrennung Sinn.

Mironsoft

Vue 3 Architektur, Frontend-Consulting und Skalierungs-Beratung

Vue-Projekt ohne klare Architektur am Wachsen?

Wir analysieren bestehende Vue-Projekte, erkennen Architektur-Schwachstellen und führen schrittweise zu einer Feature-basierten Struktur – ohne den laufenden Betrieb zu unterbrechen.

Architektur-Review

Analyse der bestehenden Vue-Struktur auf Abhängigkeiten und Skalierungshindernisse

Refactoring-Plan

Schrittweise Migration zu Feature-Slicing ohne Big-Bang-Rewrite

Team-Workshop

Architektur-Konventionen dokumentieren und ins Team übertragen

10. Zusammenfassung

Die dateibasierte **Vue-Architektur** mit Feature-Slicing löst das fundamentale Problem gewachsener Vue-Projekte: unklare Verantwortlichkeiten, zirkuläre Abhängigkeiten und mangelnde Wartbarkeit. Jedes Feature bekommt einen eigenen Ordner mit Komponenten, Composables, Store, API-Layer und Typen. Der Shared-Layer enthält nur echte, domänenfreie Abstraktion. Composables übernehmen die Geschäftslogik und halten lokalen Zustand sauber von globalem Pinia-Zustand getrennt. Routing und Lazy-Loading werden pro Feature organisiert.

Der entscheidende Schritt ist nicht die Ordnerstruktur allein, sondern die Durchsetzung der Modul-Grenzen: kein Cross-Feature-Import an internen Komponenten vorbei, kein Zustand in globalen Stores, der einem einzelnen Feature gehört. Automatisierung durch ESLint-Regeln und dependency-cruiser in der CI-Pipeline stellt sicher, dass die **Vue-Architektur** langfristig erhalten bleibt — auch wenn das Team wächst und neue Entwickler hinzukommen, die die ursprüngliche Designentscheidung nicht kennen.

Dateibasierte Vue-Architektur — Das Wichtigste auf einen Blick

Feature-Slicing

Jedes Feature hat einen eigenen Ordner unter features/ mit Komponenten, Composables, Store und API. Keine Cross-Feature-Imports an index.ts vorbei.

Composables

Composables kapseln Geschäftslogik und reaktiven Zustand. Lokaler Zustand per ref(), globaler Zustand über Pinia-Stores — nie vermischen.

Shared-Layer

Nur Abstraktionen, die von mindestens drei Features genutzt werden und keine Feature-Logik enthalten, gehören in shared/.

Grenzen durchsetzen

ESLint-Regeln und dependency-cruiser in CI verhindern, dass Modul-Grenzen über die Zeit erodieren — automatisch, ohne manuelle Code-Reviews.

11. FAQ: Dateibasierte Architektur für größere Vue-Projekte

1Ab welcher Projektgröße lohnt sich Feature-Slicing?
Ab zwei Entwicklern und mehr als fünf abgrenzbaren Features. Früh eingeführt schadet nicht, zu spät bedeutet Refactoring unter Druck.
2Wie migriert man ein bestehendes Projekt?
Schrittweise, Feature für Feature. Neue Features direkt in der neuen Struktur. Kein Big-Bang-Rewrite.
3Composable vs. Pinia-Store?
Composable = eigene Instanz pro Aufrufer. Pinia = Singleton. Pinia für Feature-übergreifend, Composables für Feature-intern.
4Darf Feature-A den Store von Feature-B importieren?
Nein. Geteilter Zustand gehört in app/stores/ oder wird über Events kommuniziert.
5Routing in Feature-Slicing?
Jedes Feature exportiert routes.ts. Der zentrale Router importiert und fügt die Arrays zusammen. Lazy-Loading erstellt automatisch Feature-Chunks.
6Was gehört in Shared?
Generische UI-Komponenten, domänenfreie Composables, Format-Utils. Kein API-Code, keine Feature-Logik.
7Shared-Layer wird zur Müllhalde?
Drei-Feature-Regel: Erst wenn drei Features etwas nutzen, kommt es in Shared. Zuerst im Feature belassen, erst bei echtem Bedarf extrahieren.
8Funktioniert mit Nuxt?
Ja. Feature-Ordner unter src/features/, Nuxt-Auto-Imports nur für echte Globals. Beide Ansätze ergänzen sich.
9Was ist die index.ts im Feature-Ordner?
Das Public API des Features. Exportiert nur das, was andere tatsächlich brauchen. Interne Details bleiben privat.
10Tools für Architektur-Grenzen?
eslint-plugin-import, dependency-cruiser in CI, madge zur Visualisierung. Machen Grenzverletzungen im PR automatisch sichtbar.