API-Versionierung
API-Versionierung hilft deiner App, Backend-Änderungen kontrolliert zu überstehen. Du lernst, Verträge stabil zu halten.
API-Versionierung ist die Disziplin, Änderungen an einer Backend-Schnittstelle so zu steuern, dass deine Android-App nicht bei jeder Server-Anpassung bricht. Für dich als App-Entwickler ist das kein reines Backend-Thema: Deine Data Layer, deine DTOs, deine Offline-Strategie und deine Release-Zyklen hängen direkt daran.
Was ist das?
API-Versionierung bedeutet, dass ein Backend mehrere Varianten eines Schnittstellenvertrags bewusst unterscheidet. Dieser Vertrag beschreibt, welche Endpunkte es gibt, welche Felder in Requests und Responses vorkommen, welche Typen sie haben und welches Verhalten du erwarten darfst. Eine Version kann sichtbar im Pfad stehen, etwa /v1/profile, über einen Header ausgehandelt werden oder über ein Feld in der Antwort erkennbar sein. Wichtig ist nicht nur die Schreibweise, sondern die Absicht: Änderungen sollen kontrollierbar, testbar und rückwärtsverträglich sein.
Das mentale Modell ist einfach zu greifen: Deine App und dein Backend sprechen über ein gemeinsames Protokoll. Wenn eine Seite Wörter entfernt, Bedeutungen ändert oder Pflichtfelder anders behandelt, versteht die andere Seite die Nachricht möglicherweise nicht mehr. Versionierung macht solche Änderungen sichtbar. Sie erlaubt Evolution, ohne Compatibility zu verlieren. Evolution heißt hier: Das System darf wachsen. Compatibility heißt: Bereits installierte Apps bleiben nutzbar, auch wenn sie nicht sofort aktualisiert werden.
In modernem Android landet dieses Thema vor allem in der Data Layer. Die UI in Jetpack Compose sollte nicht wissen, ob ein Profil aus /v1/users/me oder /v2/users/me kommt. Dein Repository, deine Remote Data Source und deine Mapper sind dafür da, externe Datenverträge in stabile App-Modelle zu übersetzen. Genau dort trennst du Netzwerkdetails von dem, was deine App fachlich braucht.
Der Roadmap-Kontext ist dabei entscheidend: Clients müssen Backend-Änderungen und schrittweise Migrationen aushalten. Android-Apps sind verteilt. Selbst wenn du heute eine neue Version veröffentlichst, nutzen manche Personen noch Wochen oder Monate eine ältere App-Version. Manche Geräte sind offline, manche Updates werden verzögert, manche Nutzer deaktivieren automatische Updates. Ein Backend, das alte App-Versionen sofort ausschließt, erzeugt echte Produktprobleme.
Deprecation gehört deshalb zur Versionierung dazu. Ein alter Endpunkt oder ein altes Feld wird nicht heimlich entfernt, sondern als veraltet markiert, beobachtet und nach einer klaren Übergangszeit abgeschaltet. Für dich heißt das: Du baust nicht nur den neuen Codepfad, sondern prüfst auch, wie lange der alte noch gebraucht wird und wie du sein Entfernen sauber absicherst.
Wie funktioniert es?
API-Versionierung funktioniert über explizite Verträge und über eine klare Grenze in deiner App-Architektur. Das Backend stellt eine Version bereit, deine App konsumiert sie über DTOs, und ein Mapper wandelt diese DTOs in Domain-Modelle um. Diese Domain-Modelle sollten möglichst stabil bleiben. Wenn sich ein JSON-Feld von display_name zu name ändert, sollte nicht deine ganze Compose-Oberfläche angepasst werden müssen. Der Unterschied gehört in die Data Layer.
In der Praxis gibt es zwei Arten von Änderungen. Kompatible Änderungen erweitern den Vertrag, ohne bestehende Clients zu beschädigen. Ein neues optionales Feld ist meist kompatibel, solange alte Apps es ignorieren können. Auch ein neuer Endpunkt ist in der Regel unkritisch, wenn alte Endpunkte weiter funktionieren. Nicht kompatible Änderungen brechen Erwartungen: Ein Feld wird entfernt, ein Typ ändert sich von String zu Objekt, ein bisher optionales Feld wird verpflichtend, oder eine Response bekommt eine neue Bedeutung. Für solche Änderungen brauchst du eine neue Version oder eine Übergangsstrategie.
Für Android mit Kotlin ist Robustheit beim Parsen wichtig. JSON-Parser wie kotlinx.serialization oder Moshi können unbekannte Felder ignorieren, wenn sie passend konfiguriert sind. Trotzdem ist das keine Ausrede für unklare Verträge. Du solltest bewusst entscheiden, welche Felder fachlich notwendig sind und welche optional bleiben dürfen. Ein fehlender Avatar ist etwas anderes als eine fehlende Nutzer-ID. Die Data Layer sollte diesen Unterschied ausdrücken.
Offline-first verstärkt das Thema. Wenn deine App Daten lokal speichert, etwa in Room, kann sie Daten aus einer alten API-Version im Cache haben, während das Backend schon eine neue Version liefert. Dann hast du nicht nur Netzwerk-Versionierung, sondern auch lokale Datenmigration. Der Repository-Code muss entscheiden, welche Datenquelle gewinnt, wie veraltete Felder behandelt werden und ob gespeicherte Daten aktualisiert werden müssen. Eine gute Regel lautet: Externe Versionsdetails dürfen nicht ungefiltert in deine lokale Datenbank und nicht ungefiltert in deine UI wandern.
Versionierung kann auf mehreren Ebenen stattfinden. Pfadversionierung wie /api/v1/articles ist leicht sichtbar und gut zu debuggen. Header-Versionierung hält URLs stabiler, ist aber weniger offensichtlich, wenn du Logs liest. Feldbasierte Versionierung kann bei einzelnen Response-Formaten helfen, wird aber schnell unübersichtlich, wenn zu viel Logik daran hängt. Für Android-Lernende ist der wichtigste Punkt: Wähle nicht nach Geschmack, sondern nach Wartbarkeit, Debugbarkeit und Team-Konvention.
Im Tagesgeschäft taucht API-Versionierung bei scheinbar kleinen Aufgaben auf. Du ergänzt ein neues Feature und brauchst ein neues Feld. Du ersetzt einen alten Endpunkt durch eine performantere Variante. Du baust einen Compose-Screen, der Daten aus einer neuen Response zeigt, aber ältere App-Versionen laufen weiterhin. Du schreibst Tests für einen Mapper, weil eine Response aus Produktion einen unerwarteten Null-Wert enthielt. All das ist API-Versionierung in realer Arbeit.
In der Praxis
Nimm an, deine App zeigt ein Nutzerprofil. In Version 1 liefert das Backend display_name. In Version 2 heißt das Feld name, zusätzlich gibt es ein optionales avatar_url. Deine UI soll davon nichts wissen. Sie erwartet nur ein stabiles Modell UserProfile.
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
data class UserProfile(
val id: String,
val name: String,
val avatarUrl: String?
)
@Serializable
data class UserProfileV1Dto(
val id: String,
@SerialName("display_name") val displayName: String
)
@Serializable
data class UserProfileV2Dto(
val id: String,
val name: String,
@SerialName("avatar_url") val avatarUrl: String? = null
)
class UserProfileMapper {
fun fromV1(dto: UserProfileV1Dto): UserProfile {
return UserProfile(
id = dto.id,
name = dto.displayName,
avatarUrl = null
)
}
fun fromV2(dto: UserProfileV2Dto): UserProfile {
return UserProfile(
id = dto.id,
name = dto.name,
avatarUrl = dto.avatarUrl
)
}
}
class UserRepository(
private val remote: UserRemoteDataSource,
private val mapper: UserProfileMapper
) {
suspend fun profile(): UserProfile {
return when (val response = remote.fetchProfile()) {
is ProfileResponse.V1 -> mapper.fromV1(response.body)
is ProfileResponse.V2 -> mapper.fromV2(response.body)
}
}
}
Das Beispiel zeigt eine zentrale Regel: Versionierte DTOs sind erlaubt, versionierte UI-Modelle sind meistens ein Warnsignal. Wenn du UserProfileV1ScreenState und UserProfileV2ScreenState quer durch die App reichst, verschiebst du die Backend-Migration in Bereiche, die damit wenig zu tun haben. Besser ist ein stabiler Kern im App-Code und eine schmale Übersetzungsschicht am Rand.
Eine konkrete Entscheidungsregel: Füge neue Felder zuerst optional hinzu, lies sie defensiv aus und entferne alte Felder erst, wenn du messen kannst, dass relevante App-Versionen sie nicht mehr brauchen. Das gilt besonders für Pflichtdaten. Wenn das Backend ein bisher vorhandenes Feld entfernt, muss der Android-Client entweder eine sinnvolle Ersatzlogik haben oder die neue Version gezielt anfordern. Raten im UI-Code ist keine saubere Strategie.
Eine typische Stolperfalle ist die Annahme, dass App- und Backend-Release gleichzeitig wirksam werden. Das stimmt selten. Der Server ist sofort aktualisiert, die App nicht. Deshalb sind Breaking Changes gefährlicher als sie im Sprint-Plan aussehen. Eine weitere Stolperfalle ist ein zu früher Deprecation-Schritt. Nur weil der neue Client-Code fertig ist, ist der alte Endpunkt noch nicht frei zum Löschen. Prüfe aktive Versionen, Crash-Reports, Netzwerkfehler und Support-Daten.
Beim Testen solltest du Mapper-Tests für alte und neue Responses schreiben. Lege Beispiel-JSON für beide Versionen ab und prüfe, dass daraus dasselbe Domain-Modell entsteht, sofern fachlich möglich. Teste auch fehlende optionale Felder. In einem Code-Review kannst du gezielt fragen: Bleibt die Änderung kompatibel? Gibt es eine Migrationsphase? Sind DTO und Domain-Modell getrennt? Wird ein altes Feld wirklich nicht mehr gebraucht? Gibt es Logs oder Metriken, um die Entfernung zu prüfen?
Beim Debugging helfen klare Netzwerklogs in Debug-Builds. Du solltest erkennen können, welche API-Version angefragt wurde und welche Response-Struktur zurückkam. Achte aber darauf, keine sensiblen Daten zu loggen. Für Offline-first-Apps kommt noch eine Prüfung hinzu: Starte die App mit alten lokalen Daten, aktualisiere dann über die neue API und beobachte, ob Repository und Datenbankmigration konsistent bleiben. Gerade hier entstehen Fehler, die im reinen Online-Test nicht sichtbar werden.
API-Versionierung ist auch ein Qualitätsthema. Sie zwingt dich, Annahmen aufzuschreiben: Welche Felder sind Pflicht? Welche Werte dürfen fehlen? Welche Versionen unterstützt die App? Wie lange lebt ein veralteter Endpunkt? Diese Fragen wirken organisatorisch, haben aber direkten Einfluss auf deinen Kotlin-Code. Ein sauberer Vertrag reduziert Sonderfälle in Compose, verhindert verstreute Null-Prüfungen und macht deine Data Layer belastbarer.
Fazit
API-Versionierung hilft dir, Android-Apps gegen kontrollierte Backend-Änderungen abzusichern. Denke dabei nicht nur an neue Endpunkte, sondern an Compatibility, Evolution und Deprecation als zusammenhängenden Prozess. Übe das konkret: Baue zwei DTO-Versionen für dieselbe fachliche Antwort, mappe beide auf ein stabiles Domain-Modell, schreibe Tests für beide JSON-Formate und prüfe im Review, ob alte App-Versionen die Änderung weiterhin überstehen.