stabile Schnittstellen für wachsende Teams in Vue 3
Eine schlecht designte Component API in einer geteilten Komponentenbibliothek ist Mehrarbeit für jedes Team-Mitglied, das sie nutzt. Props ohne Standardwerte, Emits ohne Typ-Contracts, fehlende Slots für Kompositionsstellen und undokumentiertes Expose-Interface machen Komponenten schwer nutzbar. Mit klaren Konventionen und TypeScript entstehen Component APIs, die stabil, erweiterbar und rückwärtskompatibel sind.
Inhaltsverzeichnis
- 1. Warum Component API Design in Teams kritisch ist
- 2. Props richtig designen: Typen, Defaults und Validierung
- 3. Emits als typsichere Contracts definieren
- 4. Slot-APIs: wann welche Slot-Strategie wählen
- 5. defineExpose: das öffentliche Imperativ-Interface
- 6. Provide/Inject für Komponenten-Familien
- 7. API-Versioning und Breaking-Change-Management
- 8. Typische Fehler beim Component API Design
- 9. Component API Patterns im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Component API Design in Teams kritisch ist
Eine Component API in Vue 3 ist der Vertrag zwischen der Komponente und allen, die sie nutzen. In einem Team mit mehreren Entwicklern nutzen Dutzende von Stellen in der Applikation dieselbe Basiskomponente. Wenn die Component API schlecht designt ist – Props mit unklaren Namen, Emits ohne Dokumentation, fehlende Slots für Anpassungsstellen –, dann ist der Schaden multipliziert durch alle Nutzungsstellen. Jede Breaking Change in einer geteilten Basiskomponente erzeugt Arbeit an allen Stellen, die sie verwenden. Gutes Component API Design amortisiert sich daher schneller als in einem Solo-Projekt.
Vue 3 und TypeScript bieten zusammen das beste Werkzeug-Set für stabile Component APIs, das im JavaScript-Ökosystem verfügbar ist. defineProps<Props>() macht Props vollständig typisiert und von TypeScript prüfbar. defineEmits<Emits>() macht Event-Contracts typsicher. Slot-Typen werden über defineSlots dokumentierbar. defineExpose() kontrolliert explizit, welches imperative Interface nach außen sichtbar ist. Diese vier Mechanismen zusammen ergeben ein Component API Design, das dem TypeScript-Compiler erlaubt, Fehler bei der Nutzung der Komponente zur Compile-Zeit zu finden – nicht zur Laufzeit und nicht durch Code-Reviews.
2. Props richtig designen: Typen, Defaults und Validierung
Das Fundament jedes Component API Designs in Vue 3 sind Props. Props sind das primäre Kommunikationsmedium von Eltern- zu Kind-Komponente und sollten so gestaltet sein, dass die häufigsten Anwendungsfälle keine Props erfordern und die seltensten Anwendungsfälle durch zusätzliche Props abgedeckt werden können. Der Grundsatz: Jede Prop, die einen sinnvollen Standardwert hat, sollte einen haben. Eine ButtonComponent, die für primäre Buttons bestimmt ist, sollte variant mit 'primary' als Standardwert haben – der Aufrufer muss nur dann etwas übergeben, wenn er eine andere Variante will.
Props-Benennung folgt klaren Konventionen im Component API Design: Boolesche Props beginnen ohne is-Präfix, wenn der Name selbst schon eindeutig ist (disabled statt isDisabled), aber mit is-Präfix, wenn der Kontext unklar wäre (isLoading, isExpanded). Props für Callback-Funktionen – das Vue 3 Alternative zu Emits in bestimmten Szenarien – beginnen mit on (onConfirm, onClose) und sind optionale Functions. Komplexe Konfigurationsobjekte als einzelne Props statt vieler flacher Props zu übergeben ist für stark konfigurierbare Komponenten wie Tabellen oder Charts das richtige Component API Muster – :columns="columnDef" statt zehn einzelner Spalten-Props.
<!-- DataTable.vue — Well-designed component API example -->
<script setup lang="ts">
// Type imports — clearly documented contract
import type { ColumnDef, SortState, PaginationState } from '@/types'
interface Props {
// Required: core data
rows: Record<string, unknown>[]
columns: ColumnDef[]
// Optional with defaults: common configuration
loading?: boolean
selectable?: boolean
pageSize?: number
emptyText?: string
// Optional without defaults: advanced features
sort?: SortState
pagination?: PaginationState
rowClass?: (row: Record<string, unknown>) => string
}
const props = withDefaults(defineProps<Props>(), {
loading: false,
selectable: false,
pageSize: 25,
emptyText: 'Keine Einträge vorhanden',
})
// Typed emits — the event contract
const emit = defineEmits<{
// Named tuple syntax: event name → argument types
'sort-change': [sort: SortState]
'page-change': [page: number]
'selection-change': [selectedIds: (string | number)[]]
'row-click': [row: Record<string, unknown>, event: MouseEvent]
}>()
// Typed slots — documents what slot props are available
const slots = defineSlots<{
// Default slot receives the row and column context
cell?: (props: { row: Record<string, unknown>; column: ColumnDef; value: unknown }) => void
// Named slots for custom header and footer
'header-extra'?: () => void
'empty-state'?: () => void
'footer'?: (props: { total: number; page: number }) => void
}>()
</script>
3. Emits als typsichere Contracts definieren
Emits sind der Rückkanal einer Vue 3 Component API – sie definieren, welche Ereignisse eine Komponente nach außen sendet und welche Daten diese Ereignisse tragen. In Vue 3 mit TypeScript nutzt man die typisierte defineEmits<{...}>()-Syntax mit Named-Tuple-Typen, die exakt die Argumentliste jedes Events festlegen. Das gibt dem TypeScript-Compiler die Information, um Fehler bei v-on:event-name-Bindings in der Eltern-Komponente zu melden, wenn falsche Typen übergeben werden. Ohne Typisierung sind Emits für den Compiler unsichtbar – ein häufiger Quell von schwer findbaren Runtime-Bugs in geteilten Komponentenbibliotheken.
Das Component API Design-Prinzip für Emits: Events benennen nach dem, was passiert ist, nicht nach dem, was die Eltern-Komponente tun soll. 'item-deleted' statt 'refresh-list', 'filter-changed' statt 'reload-data'. Das macht Komponenten universell einsetzbar – die Eltern-Komponente entscheidet, wie sie auf ein Event reagiert; die Kind-Komponente teilt nur mit, was passiert ist. Für Events mit komplexen Payloads ist ein typisiertes Interface als Argument besser als viele separate Parameter – 'form-submitted': [data: FormData] statt 'form-submitted': [name: string, email: string, message: string], weil letzteres die Erweiterung der Formulardaten zu einer Breaking Change macht.
4. Slot-APIs: wann welche Slot-Strategie wählen
Slots sind das mächtigste Kompositions-Werkzeug im Vue 3 Component API Design – und das am häufigsten unter- oder falsch eingesetzte. Ein Default-Slot eignet sich für Komponenten, die beliebigen Inhalt wrappen: BaseCard, BaseModal, BaseSection. Benannte Slots eignen sich für Komponenten mit klar definierten Inhaltsbereichen: header, body, footer bei einem Dialog, prefix und suffix bei einem Input. Scoped Slots sind das mächtigste Muster – sie übergeben Daten aus der Kind-Komponente an den Slot-Inhalt der Eltern-Komponente, was völlig flexible Darstellung bei gleichzeitig klar getrennter Logik ermöglicht.
Das Scoped-Slot-Pattern für eine Datentabelle ist exemplarisch: Die Tabellen-Komponente kümmert sich um Sortierung, Paginierung, Selektion und Layout. Wie eine einzelne Zelle gerendert wird, überlässt sie dem Aufrufer durch einen Scoped-Slot, der Zeile, Spalten-Definition und Zellwert übergibt. Das ist das Gegenteil von einem Render-Prop-Pattern aus React – es ist nativ in Vue und typisierbar mit defineSlots. Wichtig für das Component API Design: Slots sollten dokumentiert sein – welche Slots gibt es, welche Scoped-Slot-Props stehen zur Verfügung, welche Standarddarstellung wird ohne Slot gerendert. In einer Komponentenbibliothek ist diese Dokumentation genauso wichtig wie Props-Dokumentation.
<!-- Accordion.vue — Component family using provide/inject for coordination -->
<script setup lang="ts">
import { ref, provide } from 'vue'
interface Props {
// Allow multiple items open simultaneously or only one
multiple?: boolean
// Default open items by their value
defaultOpen?: string[]
}
const props = withDefaults(defineProps<Props>(), {
multiple: false,
defaultOpen: () => [],
})
const emit = defineEmits<{
'change': [openItems: string[]]
}>()
// Internal state — not exposed, managed by coordination
const openItems = ref<string[]>(props.defaultOpen)
function toggle(itemValue: string) {
if (props.multiple) {
const idx = openItems.value.indexOf(itemValue)
if (idx >= 0) {
openItems.value.splice(idx, 1)
} else {
openItems.value.push(itemValue)
}
} else {
openItems.value = openItems.value.includes(itemValue) ? [] : [itemValue]
}
emit('change', [...openItems.value])
}
function isOpen(itemValue: string) {
return openItems.value.includes(itemValue)
}
// Provide coordination API to child AccordionItem components
provide('accordion', { isOpen, toggle })
// Public API: programmatic control for parent components
defineExpose({ openAll, closeAll, openItem: toggle })
function openAll() {
// Only makes sense with multiple=true
emit('change', openItems.value)
}
function closeAll() {
openItems.value = []
emit('change', [])
}
</script>
<template>
<!-- Simple structural wrapper — no presentational logic -->
<div role="presentation">
<slot />
</div>
</template>
5. defineExpose: das öffentliche Imperativ-Interface
In Vue 3 mit <script setup> ist standardmäßig nichts von einer Komponente nach außen zugänglich – kein State, keine Methoden. Das ist ein bewusstes Design-Entscheidung, die versehentliche Kopplungen verhindert. defineExpose() öffnet explizit bestimmte Methoden oder Ref-Werte für den Eltern-Zugriff über Template-Refs. Das Component API Design-Prinzip: Expose sparingly – nur das exponieren, was wirklich imperativ von außen gesteuert werden muss. Ein Modal exponiert open() und close(). Ein Formular exponiert validate() und reset(). Ein Video-Player exponiert play(), pause() und seek(timestamp). State wie isOpen oder currentTime sollte stattdessen als Emit kommuniziert werden, nicht direkt lesbar gemacht.
TypeScript-Typisierung für exponierte Interfaces erreicht man durch die Deklaration eines expliziten Types: export type ModalInstance = { open: () => void; close: () => void }. Im Eltern-Kontext nutzt man dann useTemplateRef<ModalInstance>('modal'), was vollständige Autocomplete und Typ-Prüfung beim Aufruf von modal.value?.open() ermöglicht. Diese Praxis ist in Komponentenbibliotheken, die npm-Packages sind, besonders wichtig: Externe Nutzer sehen das exponierte Interface als Teil der öffentlichen Component API und verlassen sich darauf. Änderungen am Expose-Interface sind Breaking Changes und müssen als solche kommuniziert werden.
6. Provide/Inject für Komponenten-Familien
Provide/Inject ist das Vue 3 Component API Pattern für Komponenten-Familien: Gruppen zusammengehöriger Komponenten, die miteinander koordinieren müssen, ohne dass Props durch viele Zwischenebenen weitergegeben werden. Das Paradebeispiel ist ein Accordion mit Accordion-Container und AccordionItem-Kindern: Der Container hält den offenen/geschlossenen Zustand und exponiert eine Koordinations-API über provide(). Jedes AccordionItem ruft inject() auf, um isOpen und toggle zu erhalten – ohne Props, ohne Event-Bubbling durch dazwischenliegende Elemente.
Das typsichere Provide/Inject-Pattern in Vue 3 nutzt typed Injection Keys: const AccordionKey = Symbol() as InjectionKey<AccordionContext>. Das Symbol garantiert Einzigartigkeit des Keys, der TypeScript-Typ garantiert, dass inject(AccordionKey) immer AccordionContext | undefined zurückgibt und nicht einfach unknown. Ein Guard const ctx = inject(AccordionKey); if (!ctx) throw new Error('AccordionItem must be inside Accordion') gibt eine klare Fehlermeldung, wenn jemand AccordionItem außerhalb des Containers verwendet. Dieses Component API Pattern macht Komponenten-Familien robust und ihre Nutzungsregeln selbst-dokumentierend.
7. API-Versioning und Breaking-Change-Management
In einer geteilten Komponentenbibliothek für ein Team ist Component API Versioning ein unterschätztes Thema. Wenn eine Komponente in zwanzig Stellen der Applikation genutzt wird, ist das Umbenennen eines Props eine Breaking Change, die zwanzig gleichzeitige Änderungen erfordert. Das Component API Design-Prinzip für Rückwärtskompatibilität: neue Props optional hinzufügen, alte Props nie sofort entfernen. Für Umbenennungen gibt es eine Deprecation-Periode, in der die alte Prop noch akzeptiert wird und eine Laufzeit-Warnung in der Entwicklungsumgebung ausgibt, wenn sie genutzt wird. Erst in der nächsten Major-Version wird die veraltete Prop entfernt.
Semantic Versioning ist für interne Komponentenbibliotheken der verlässlichste Weg, Breaking Changes zu kommunizieren. Jede Breaking Change bedeutet eine neue Major-Version. Neue Features ohne Breaking Changes bedeuten Minor-Versionen. Bug-Fixes sind Patch-Versionen. Mit npm-Workspaces oder dem Vite Library-Mode als npm-Package kann eine Komponentenbibliothek versioned und als Dependency in anderen Teilen des Monorepos eingebunden werden. Das ermöglicht, unterschiedliche Teile der Applikation auf unterschiedlichen Versionen der Komponentenbibliothek zu halten, während die Migration auf neue Versionen schrittweise erfolgt. Diese Praxis macht Component API Evolution planbar statt chaotisch.
// types/injection-keys.ts — Typed injection keys for component families
import type { InjectionKey, Ref } from 'vue'
// AccordionContext — shared state between Accordion and AccordionItem
export interface AccordionContext {
isOpen: (value: string) => boolean
toggle: (value: string) => void
multiple: boolean
}
// Type-safe injection key — Symbol ensures uniqueness
export const AccordionKey: InjectionKey<AccordionContext> = Symbol('Accordion')
// TabsContext — shared state between Tabs and Tab
export interface TabsContext {
activeTab: Ref<string>
setActiveTab: (value: string) => void
orientation: 'horizontal' | 'vertical'
}
export const TabsKey: InjectionKey<TabsContext> = Symbol('Tabs')
// Usage in AccordionItem.vue:
// const ctx = inject(AccordionKey)
// if (!ctx) throw new Error('AccordionItem must be a child of Accordion')
// const isOpen = computed(() => ctx.isOpen(props.value))
// Usage pattern with provide in Accordion.vue:
// provide(AccordionKey, { isOpen, toggle, multiple: props.multiple })
8. Typische Fehler beim Component API Design
Der häufigste Fehler im Component API Design ist die God-Component: eine Komponente, die durch immer mehr Props für immer mehr Anwendungsfälle erweitert wird, bis ihre API dreißig Props hat und keine davon gut verständlich ist. Das Symptom sind Props, die sich gegenseitig widersprechen oder nur in bestimmten Kombinationen Sinn ergeben. Die Lösung ist Komposition statt Konfiguration: statt einer einzigen Input-Komponente mit zwanzig Props lieber spezialisierte Varianten wie TextInput, NumberInput, CurrencyInput, die alle ein gemeinsames BaseInput-Composable nutzen. Jede davon hat eine kleine, verständliche Component API statt einer gemeinsamen, unverständlichen.
Ein zweiter verbreiteter Fehler ist das fehlende Slot-Design. Wer in einer Komponente keine Slots vorsieht, muss für jeden Anpassungswunsch eine neue Prop hinzufügen – das führt direkt zur God-Component. Das richtige Component API Design sieht Slots als Erweiterungspunkte vor: Wo ist es plausibel, dass jemand anderen Inhalt einfügen will? Header, Footer, leere Zustände, Aktions-Bereiche – das sind typische Slot-Punkte. Ein dritter Fehler: Kein eindeutiger Ownership von State. Wenn unklar ist, ob die Eltern-Komponente oder die Kind-Komponente den Zustand einer Interaktion hält, entstehen inkonsistente Verhaltensweisen. Das Component API Design-Pattern für unkontrollierte/kontrollierte Komponenten: Wenn v-model nicht verwendet wird, hält die Komponente ihren eigenen State (unkontrolliert). Wenn v-model verwendet wird, ist die Eltern-Komponente der Single Source of Truth (kontrolliert).
9. Component API Patterns im direkten Vergleich
Im Component API Design für Vue 3 Teams gibt es für jede Situation ein besseres und ein schlechteres Muster. Die folgende Tabelle zeigt die häufigsten Entscheidungen und ihre Konsequenzen.
| Situation | Fragiles Muster | Stabiles Component API Pattern | Vorteil |
|---|---|---|---|
| Komponentenanpassung | 30 Props für jeden Anwendungsfall | Scoped Slots für Flexibilität | Keine God-Component, erweiterbar ohne Breaking Change |
| Komponenten-Familie | Props durch 5 Ebenen | provide/inject mit InjectionKey | Typsicher, direkt, keine Prop-Drilling |
| Imperatives Interface | Ref auf interne Methoden | defineExpose mit Typ | Explizites öffentliches API, kein internes Leck |
| Breaking Change | Prop sofort umbenennen | Deprecation + Laufzeit-Warnung | Schrittweise Migration, keine Big-Bang-Änderungen |
| Formular-Integration | Direkter State in Kind-Komponente | defineModel() für v-model | Eltern hält State, Kind rendert – klare Ownership |
Die Tabelle zeigt: Gutes Component API Design ist keine einzelne Technik, sondern ein Set von Konventionen, die konsequent angewendet werden müssen. Teams, die diese Konventionen explizit dokumentieren – in einem CONTRIBUTING.md oder einem Architecture Decision Record –, haben weniger Diskussionen bei Code-Reviews und konsistentere Komponentenbibliotheken.
Mironsoft
Vue 3 Komponentenbibliotheken, API-Design und Frontend-Architektur für Teams
Komponentenbibliothek, auf die Ihr Team sich verlässt?
Wir designen und bauen Vue 3 Komponentenbibliotheken mit stabilen, typsicheren Component APIs – Props-Konventionen, Slot-Strategie, defineExpose-Interfaces und Versioning-Strategie für wachsende Teams.
API-Audit
Bestehende Komponentenbibliotheken auf Props-Konsistenz, Breaking-Change-Risiken und Slot-Strategie prüfen
Bibliotheks-Aufbau
Vue 3 Komponentenbibliothek mit Vite, Vitest, TypeScript und vollständiger API-Dokumentation
Team-Guidelines
Component API Konventionen, Naming-Guidelines und Versioning-Strategie für Ihr Team dokumentieren
10. Zusammenfassung
Gutes Component API Design in Vue 3 ist eine Investition, die sich über die gesamte Lebensdauer einer Komponentenbibliothek auszahlt. Props-Typisierung mit defineProps<Props>() und withDefaults gibt jedem Prop einen klaren Vertrag und Standardwert. Emit-Kontrakte mit defineEmits<{...}>() machen Events zu TypeScript-geprüften Schnittstellen. Scoped Slots mit defineSlots ermöglichen Anpassung ohne God-Component-Wachstum. defineExpose() kontrolliert explizit, welches imperative Interface nach außen zugänglich ist. Provide/Inject mit typisierten Injection Keys koordiniert Komponenten-Familien ohne Prop-Drilling.
Der größte Hebel liegt in der Konsequenz. Eine Komponentenbibliothek, die für die ersten Komponenten solide Component API Design-Prinzipien befolgt, für die nächsten zehn aber unter Zeitdruck auf schnelle Lösungen zurückgreift, ist inkonsistent und schwer nutzbar. Teams, die explizite Konventionen für Props-Benennung, Slot-Design, Event-Naming und Versioning dokumentieren und in Code-Reviews durchsetzen, bekommen Komponentenbibliotheken, die mit dem Team wachsen – statt die Entwicklungsgeschwindigkeit im Laufe der Zeit zu senken.
Component APIs designen — Das Wichtigste auf einen Blick
Props & Emits
defineProps<Props>() mit withDefaults. Emits als Named-Tuple-Typen. Events benennen nach was passiert ist, nicht was die Eltern tun sollen.
Slots für Flexibilität
Scoped Slots als Erweiterungspunkte statt wachsender Props-Liste. defineSlots für TypeScript-Dokumentation der Slot-Contracts.
Expose & Inject
defineExpose sparsam – nur echte imperative APIs. Typed Injection Keys für Komponenten-Familien ohne Prop-Drilling.
Versioning
Semantic Versioning, Deprecation-Periode mit Laufzeit-Warnung, kein sofortiges Umbenennen. Breaking Changes kündigen, nicht überraschend einführen.