Die Entscheidung, ein funktionierendes Frontend zu migrieren, ist selten trivial. Doch genau solche Projekte prägen Entwickler:innen – besonders, wenn sie auf realen Codebasen stattfinden. Ein studentisches Team der Rise Academy stand vor dieser Herausforderung mit Bloom After, einer Anwendung, die sich durch Inkonsistenzen und veraltete Praktiken auszeichnete. Die Lösung? Ein moderner Tech-Stack mit Next.js und TypeScript.
Warum eine Migration trotz funktionierendem System sinnvoll war
Die ursprüngliche Version von Bloom After lief stabil, doch der Code entsprach nicht mehr den Anforderungen eines wachsenden Projekts. Die Aufgabenstellung der Rise Academy bestand darin, die Anwendung auf ein modernes Framework zu übertragen – nicht wegen technischer Defizite, sondern als praktische Übung. Diese Herangehensweise spiegelt den realen Arbeitsalltag wider: Migrationen sind selten Notfallmaßnahmen, sondern geplante Schritte zur Verbesserung der Codebasis.
Während der Arbeit traten jedoch konkrete Probleme zutage, die behoben werden mussten:
- Uneinheitliche Datenstrukturen: Backend-Antworten nutzten teils
_id, teilsidsowiedescoderdescription– ein Albtraum für jede Codebasis. - Authentifizierung via `localStorage`: Eine fragwürdige Praxis, die Cross-Site-Scripting-Angriffe begünstigt.
- Fehlende Code-Struktur: Ohne klare Vorgaben neigten Teammitglieder dazu, unterschiedliche Lösungen für ähnliche Probleme zu implementieren.
Das primäre Ziel war nicht die Rettung eines kaputten Systems, sondern die Anwendung bewährter Migrationsstrategien auf eine realistische Codebasis.
Strategische Vorarbeiten: Architekturentscheidungen, die Zeit sparten
Bevor das Team mit der eigentlichen Entwicklung begann, wurden grundlegende Entscheidungen getroffen, die spätere Konflikte verhinderten.
Konsistente CSS-Klassennamen beibehalten
Statt das gesamte Stylesheet neu zu schreiben, entschied sich das Team, alle bestehenden CSS-Klassennamen beizubehalten. Jede Seite importiert lediglich die benötigten Stile – ein globales Stylesheet wurde vermieden. Diese Strategie brachte mehrere Vorteile mit sich:
- Keine doppelten Designarbeiten
- Keine visuellen Regressionen durch falsche Klassennamen
- Entwickler:innen und Designer:innen konnten auf vertraute Namen zurückgreifen
Route-Gruppen für geteilte Layouts nutzen
Next.js unterstützt Route-Gruppen, die zwar keinen Einfluss auf die URL haben, aber gemeinsame Layouts ermöglichen. Das Team strukturierte die Anwendung wie folgt:
app/
├── (public)/ # Marketing-Seiten – gemeinsamer öffentlicher Header
├── (admin)/ # Admin-Bereich – gemeinsamer Admin-Header
└── (dashboard)/ # Nutzer-Dashboard – gemeinsame SeitenleisteJede Gruppe verfügt über eine eigene layout.tsx-Datei, die automatisch das richtige Layout rendert – ohne bedingte Logik in den Komponenten.
Daten normalisieren – noch bevor die ersten Komponenten entstanden
Ein zentraler Schritt war die Definition von TypeScript-Schnittstellen, die die gewünschte Datenstruktur widerspiegelten – unabhängig von den tatsächlichen Backend-Antworten. Anschließend wurden Normalisierungsfunktionen erstellt, um die unordentlichen API-Antworten in saubere, vorhersehbare Strukturen zu überführen.
export interface Kurs {
id: string; // Normalisiert aus _id oder id
titel: string;
beschreibung: string; // Normalisiert aus desc oder description
erstelltAm: string;
}
export function normalisiereKurs(rohdaten: Record<string, unknown>): Kurs {
return {
id: (rohdaten._id ?? rohdaten.id) as string,
titel: rohdaten.titel as string,
beschreibung: (rohdaten.description ?? rohdaten.desc) as string,
erstelltAm: rohdaten.erstelltAm as string,
};
}Diese Normalisierungsschicht erwies sich als einer der größten Vorteile von TypeScript: Sobald sie implementiert war, konnten alle nachgelagerten Komponenten auf eine konsistente Datenstruktur vertrauen.
Woche 1: Fundament legen
Startseite und erste Komponenten
Um die gesamte Infrastruktur – Routing, CSS-Importe, Layouts – vorab zu validieren, begann das Team mit der Index-Seite. Erst nach deren Fertigstellung wurden die ersten gemeinsamen Komponenten entwickelt:
- Teammitglieder: Zeigt das Team auf öffentlichen Seiten an
- Vorschlagsfenster: Ein schiebbares Fenster für Nutzer:innen-Feedback
Diese Komponenten dienten als Referenz für die Projektstruktur, bevor weitere Teammitglieder Beiträge leisteten.
Zentrale API-Schicht für konsistente Anfragen
Statt jedem Entwickler die Freiheit zu geben, eigene fetch-Aufrufe zu schreiben, wurde eine zentrale API-Schicht erstellt:
// lib/api.ts
const API_BASE_URL = process.env.NEXT_PUBLIC_API_URL;
async function anfrage<T>(
methode: string,
endpunkt: string,
daten?: unknown
): Promise<T> {
const antwort = await fetch(`${API_BASE_URL}${endpunkt}`, {
method: methode,
headers: { "Content-Type": "application/json" },
body: daten ? JSON.stringify(daten) : undefined,
credentials: "include",
});
if (!antwort.ok) {
throw new Error(`${methode} ${endpunkt} → ${antwort.status}`);
}
return antwort.json();
}
export const api = {
get: <T>(endpunkt: string) => anfrage<T>("GET", endpunkt),
post: <T>(endpunkt: string, daten: unknown) => anfrage<T>("POST", endpunkt, daten),
patch: <T>(endpunkt: string, daten: unknown) => anfrage<T>("PATCH", endpunkt, daten),
del: <T>(endpunkt: string) => anfrage<T>("DELETE", endpunkt),
};Für den lifestyle-Backend-Bereich wurde zusätzlich eine spezifische Wrapper-Funktion erstellt, die alle Anfragen an /lifestyle bündelt. Dies ermöglichte es Teammitgliedern, ohne Kenntnis der gesamten Backend-Struktur auf Daten zuzugreifen.
Woche 2: Admin-Bereich, Authentifizierung und Zugriffskontrolle
Migration von localStorage zu sicheren Cookies
Die ursprüngliche Anwendung speicherte Authentifizierungstokens in localStorage – eine Praxis, die anfällig für Cross-Site-Scripting-Angriffe ist. Die Lösung bestand in der Integration von NextAuth mit httpOnly-Cookies, die serverseitige Authentifizierungsprüfungen ermöglichen.
Die wichtigsten Schritte:
- Benutzerdefinierte API-Routen für Login/Logout:
// app/api/auth/login/route.ts
import { cookies } from "next/headers";
export async function POST(anfrage: Request) {
const { email, passwort } = await anfrage.json();
const antwort = await fetch(`${process.env.API_URL}/auth/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, passwort }),
});
if (antwort.ok) {
const { token } = await antwort.json();
cookies().set("auth_token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: 60 * 60 * 24 * 7, // 1 Woche
});
}
return antwort;
}Diese Migration verbesserte nicht nur die Sicherheit, sondern ermöglichte auch serverseitige Authentifizierungsprüfungen – ein entscheidender Vorteil für zukünftige Erweiterungen.
Die Migration von Bloom After zeigt, dass selbst funktionierende Systeme von modernen Frameworks profitieren können. Durch strategische Vorarbeiten, konsistente Datenstrukturen und eine klare Architektur wurde der Übergang nicht nur reibungslos, sondern auch lehrreich. Die gesammelten Erfahrungen werden dem Team nicht nur in zukünftigen Projekten zugutekommen, sondern auch anderen Entwickler:innen als Best-Practice-Beispiel dienen. Die nächste Herausforderung? Die Anwendung um Echtzeit-Funktionen zu erweitern.
KI-Zusammenfassung
Learn how a student team migrated a legacy JavaScript frontend to Next.js with TypeScript, including authentication, route groups, and API centralization strategies.