Bild von Saeid Samani
Saeid Samani
Saeid Samani ist Webentwickler, kreativer Denker und Unternehmer mit einem tiefen Verständnis für digitale Prozesse und moderne Webtechnologien. Er entwickelt durchdachte Webanwendungen, optimierte Benutzererlebnisse und skalierbare Lösungen, die Design, Funktionalität und Strategie miteinander verbinden. In seinem Blog teilt Saeid seine Erfahrungen aus der täglichen Praxis – von sauberem Code und effizientem Workflow bis hin zu Themen rund um Webentwicklung, Automatisierung und digitale Trends. Sein Stil: klar, pragmatisch und immer mit dem Ziel, Wissen greifbar und anwendbar zu machen. Abseits der Arbeit liebt er es, Neues zu lernen, Ideen auszuprobieren und Wege zu finden, Technologie sinnvoll im Alltag einzusetzen.

Teil 5 – Von der API zum UI: Service-Layer, Pinia und automatische Webinar-Auswahl in Vue 3

In diesem Beitrag erfahren Sie, wie Sie Ihre Vue-3-App strukturiert an eine externe API anbinden, einen klaren Service-Layer aufbauen und mit Pinia den State Ihrer Webinare verwalten. Sie lernen, wie automatisch das nächste passende Webinar ausgewählt wird und wie Ihr Store die HomeView.vue mit den richtigen Daten versorgt.

Inhaltsverzeichnis

Projektvorbereitung: Ihr Vue-3-Setup für echte API-Daten

Bevor Sie mit der eigentlichen Umsetzung beginnen, lohnt sich ein kurzer Blick auf die Grundlagen von Vite und TypeScript. Beide Werkzeuge bilden das Fundament Ihres modernen Vue-3-Projekts – Vite sorgt für das schnelle Build- und Dev-Setup, TypeScript für klare Typisierung und sauberen Code.

Einführung in die tsconfig.json

Eine der zentralen Konfigurationsdateien in jedem TypeScript-Projekt ist die tsconfig.json.
Hier legen Sie fest, wie der TypeScript-Compiler Ihren Code interpretiert, überprüft und in JavaScript übersetzt. Diese Datei definiert also, welche Sprachfeatures aktiv sind, welche Module eingebunden werden dürfen und welche Verzeichnisse beim Kompilieren berücksichtigt werden.

Zu den wichtigsten Punkten gehören:

  • Compiler-Optionen: Hier steuern Sie, auf welche JavaScript-Version kompiliert wird (target), welches Modul-System genutzt wird (module), und ob strikte Typprüfungen aktiv sind (strict).
  • Pfadangaben und Alias: Über baseUrl und paths können Sie eigene Import-Alias definieren – etwa @ → src/, um Importe übersichtlicher zu gestalten.
  • Include / Exclude: Damit bestimmen Sie, welche Dateien oder Ordner TypeScript beim Kompilieren einbeziehen oder ignorieren soll.
  • Type-Definitionen: Unter types können Sie zusätzliche Typ-Pakete (z. B. für Node oder Vite) einbinden.

Mit dieser Datei schaffen Sie also die Grundlage, dass Ihr Code nicht nur funktioniert, sondern typensicher, wartbar und skalierbar bleibt.

Damit Ihr Vue-3-Projekt mit Vite und modernen Sprachfeatures reibungslos läuft, empfehle ich folgende Einstellungen:

{
  "compilerOptions": {
    "target": "ESNext",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"]
    },
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]
}

Kopieren Sie diese Konfiguration direkt in Ihre eigene tsconfig.json.
Im Folgenden erfahren Sie, was jede einzelne Einstellung bewirkt

Erklärung der tsconfig.json Zeile für Zeile

{
  "compilerOptions": {

→ Beginn des Abschnitts, in dem alle Optionen für den TypeScript-Compiler definiert werden.

    "target": "ESNext",

→ Legt fest, auf welche JavaScript-Version TypeScript den Code kompiliert.
ESNext bedeutet: immer auf den neuesten verfügbaren ECMAScript-Standard (z. B. ES2023 oder später).
So stehen moderne Sprachfeatures wie async/await, optionale Verkettung (?.) oder Top-Level-await zur Verfügung.

    "useDefineForClassFields": true,

→ Aktiviert die neue ECMAScript-konforme Art, Klassenfelder zu definieren.
Damit verhält sich TypeScript beim Umgang mit class-Eigenschaften exakt wie modernes JavaScript.

    "module": "ESNext",

→ Gibt an, dass beim Kompilieren ECMAScript-Module (import / export) verwendet werden.
Das ist die Standard-Modulstruktur für Vite und moderne Browser.

    "moduleResolution": "Node",

→ Bestimmt, wie TypeScript beim Auflösen von Importen vorgeht.
Mit "Node" wird das gleiche Verhalten wie in Node.js verwendet:
TypeScript sucht Module in node_modules und erkennt automatisch Erweiterungen wie .ts, .vue oder .d.ts.

    "strict": true,

→ Aktiviert den Strict Mode, also alle strengen Typprüfungen.
Dadurch meldet TypeScript auch kleinste Typabweichungen und hilft, viele Laufzeitfehler frühzeitig zu vermeiden.

    "jsx": "preserve",

→ Weist TypeScript an, JSX-Syntax (z. B. aus .tsx– oder Vue-Dateien mit <script setup lang="tsx">) unverändert zu lassen.
Vite übernimmt später die Verarbeitung von JSX-Code.

    "baseUrl": ".",

→ Legt den Basisordner für Modulauflösungen fest.
Der Punkt (".") steht für das Projekt-Root.
So können relative Importpfade kürzer und übersichtlicher gestaltet werden.

    "paths": {
      "@/*": ["src/*"]
    },

→ Definiert einen Alias für Importe.
@ verweist auf den src-Ordner.
Damit können Sie statt ../../../components/Button.vue einfach @/components/Button.vue importieren.

    "resolveJsonModule": true,

→ Erlaubt das Importieren von JSON-Dateien direkt im TypeScript-Code.
Zum Beispiel:

import config from '@/data/config.json';
    "isolatedModules": true,

→ Erzwingt, dass jede Datei isoliert kompilierbar ist.
Das ist wichtig, wenn Vite einzelne Module unabhängig voneinander verarbeitet.
Verhindert typische Fehler bei fehlenden Typinformationen oder kreisförmigen Abhängigkeiten.

    "esModuleInterop": true,

→ Erlaubt den Import von CommonJS-Modulen (z. B. alten npm-Paketen).
Ohne diese Option müssten Importe oft umständlich mit import * as … geschrieben werden.

    "lib": ["ESNext", "DOM"],

→ Gibt an, welche Typdefinitionen TypeScript kennen soll.
ESNext aktiviert alle modernen Sprachfeatures,
DOM stellt Browser-APIs (z. B. document, window, fetch) bereit.

    "skipLibCheck": true

→ Überspringt die Typprüfung externer Bibliotheken in node_modules.
Das spart Zeit beim Kompilieren, ohne Auswirkungen auf Ihren eigenen Code.

  },

→ Ende des Abschnitts mit Compiler-Optionen.

  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],

→ Bestimmt, welche Dateien TypeScript beim Kompilieren einbezieht.
Hier also alle .ts, .tsx, .d.ts und .vue-Dateien im src-Verzeichnis (rekursiv in allen Unterordnern).

  "references": [
    { "path": "./tsconfig.app.json" },
    { "path": "./tsconfig.node.json" }
  ]

→ Verweist auf zwei zusätzliche Konfigurationsdateien,
die Vite automatisch verwendet:

  • tsconfig.app.json – für den Browser-Code
  • tsconfig.node.json – für Node-Skripte (z. B. Vite-Build-Prozesse)

Das sorgt für getrennte, optimierte Build-Umgebungen.

}

→ Schließt die Konfigurationsdatei ab.

Ihre vite.config.ts: das Herzstück der Projektkonfiguration

Nachdem Sie die tsconfig.json eingerichtet haben, folgt die zweite zentrale Konfigurationsdatei Ihres Vue-3-Projekts: vite.config.ts.

Diese Datei ist vergleichbar mit der tsconfig.json, übernimmt aber eine andere Rolle:
Während die tsconfig.json für TypeScript zuständig ist (also wie Ihr Code geprüft und kompiliert wird), steuert vite.config.ts die gesamte Build- und Entwicklungsumgebung.

Hier legen Sie fest, welche Plugins geladen werden, wie Module aufgelöst werden, wo Ihre Quellcodes liegen und wie Vite das Projekt beim Entwickeln und Bauen behandelt.

Im Folgenden sehen Sie eine empfohlene Basisversion, wie sie auch in Ihrem Projekt verwendet wird:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

// https://vite.dev/config/
export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
})

Zeile für Zeile erklärt

import { defineConfig } from 'vite'

→ Importiert die Hilfsfunktion defineConfig aus Vite.
Sie sorgt für eine bessere TypeScript-Unterstützung und ermöglicht, dass die IDE alle Optionen mit Autovervollständigung erkennt.

import vue from '@vitejs/plugin-vue'

→ Bindet das offizielle Vue-Plugin ein.
Dieses Plugin ist notwendig, damit Vite .vue-Dateien versteht, die Template-, Script- und Style-Blöcke korrekt trennt und Hot-Module-Reload (HMR) aktiviert.

import path from 'path'

→ Importiert das Node.js-Modul path, das beim Arbeiten mit Dateipfaden hilft.
Wir nutzen es gleich, um den absoluten Pfad für den @-Alias aufzulösen.

export default defineConfig({

→ Exportiert die gesamte Konfiguration als Standard-Export.
Vite liest beim Start automatisch diese Datei ein und verwendet sie als Hauptkonfiguration.

plugins: [vue()],

→ Hier werden die gewünschten Plugins eingebunden.
In diesem Fall nur das Vue-Plugin, das .vue-Dateien verarbeitet.
Sie können hier später weitere Plugins hinzufügen, z. B. für SVG-Handling, Auto-Imports oder Komponenten-Resolver.

resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
},
},

→ Der Abschnitt resolve.alias definiert alternative Importpfade.
Der Alias @ wird hier auf den absoluten Pfad zum src-Verzeichnis gesetzt.
Dadurch können Sie im gesamten Projekt kurze, einheitliche Importe schreiben:

Beispiel:

import HomeView from '@/views/HomeView.vue'

statt

import HomeView from '../../../views/HomeView.vue'

Diese Alias-Definition entspricht genau dem Pfad, den Sie bereits in Ihrer tsconfig.json hinterlegt haben – so sind Vite und TypeScript perfekt aufeinander abgestimmt.

})

→ Schließt die Konfiguration.

Umgebungsvariablen: API-URLs sauber über .env

In einem Vite-Projekt kann man mehrere Varianten von .env-Dateien anlegen, um Konfigurationen für unterschiedliche Umgebungen wie Entwicklung, Test oder Produktion sauber zu trennen. Die einfachste und immer geladene Datei ist .env. Sie enthält globale Standardwerte, die für alle Umgebungen gelten. Ergänzend dazu gibt es .env.local, eine lokale Variante, die nicht ins Git-Repository gehört und meist sensible Daten wie API-Keys oder Zugangsdaten enthält.

Darüber hinaus unterstützt Vite das Konzept von sogenannten Modes. Standardmäßig gibt es „development“ und „production“, aber man kann auch eigene Modi wie „staging“ definieren. Für jeden dieser Modi lässt sich eine eigene Datei anlegen, zum Beispiel .env.development, .env.production oder .env.staging. Diese werden jeweils nur geladen, wenn Vite mit dem entsprechenden Modus gestartet wird – etwa beim lokalen Entwickeln (vite oder vite dev) oder beim Erstellen des Builds (vite build).

Zusätzlich kann jede dieser Dateien auch eine lokale Variante haben, etwa .env.development.local oder .env.production.local. Diese überschreiben die gleichnamigen Standarddateien und sind nützlich, wenn man lokal andere Werte benötigt, ohne dass sie im Team geteilt werden. Auch diese Dateien sollten nicht versioniert werden.

Die Reihenfolge, in der Vite die Dateien lädt, ist klar festgelegt: Zuerst .env.[mode].local, danach .env.local, dann .env.[mode] und zuletzt .env. Dadurch haben lokale Dateien immer Vorrang vor globalen.

Ein typisches Setup sieht also so aus, dass man in .env allgemeine Werte wie den Projektnamen oder Standard-URLs definiert, in .env.development die lokale API-Adresse (http://localhost:8080) einträgt und in .env.production die produktive URL (https://api.productionserver.com). Private Schlüssel oder geheime Tokens landen ausschließlich in .env.local.

Wichtig ist, dass nur Variablen, die mit VITE_ beginnen, im Frontend verfügbar sind. Auf sie kann man im Code über import.meta.env.VITE_API_BASE_URL oder ähnliche Schlüssel zugreifen. Alles andere bleibt intern im Build-Prozess und wird nicht an den Browser weitergegeben.

Dieses System erlaubt es, Projekte flexibel und sicher zu konfigurieren – mit klarer Trennung zwischen gemeinsam genutzten Werten und privaten Daten, die nur auf dem lokalen Rechner existieren sollen.

Wenn du in einer .env-Datei eine Variable mit dem Präfix VITE_ definierst, wird sie beim Build-Prozess direkt in den Frontend-Code eingebaut. Das heißt:
sie landet im fertigen JavaScript-Bundle, das der Browser herunterlädt.
Das bedeutet wiederum, dass alle Werte mit VITE_ öffentlich sichtbar sind – man kann sie im Browser-DevTools oder im Quelltext des kompilierten Codes finden.

Deshalb dürfen niemals geheime Informationen wie API-Keys, Tokens, Passwörter oder private URLs in VITE_-Variablen stehen. Diese sind ausschließlich für den Build bestimmt, nicht für den Server.

Wenn du also wirklich vertrauliche Daten brauchst, hast du zwei sichere Alternativen:

  1. Serverseitige Variablen:
    Solche Keys gehören in eine .env-Datei ohne VITE_-Präfix. Sie werden im Node-Kontext oder beim Server-Build gelesen, aber nicht an den Browser weitergegeben.
    → Beispiel: API_SECRET_KEY=123abc Und dann nur im Server-Code (nicht im Vue-Frontend) darauf zugreifen.
  2. Proxy oder API-Gateway:
    Wenn du im Frontend mit einer API kommunizierst, die Authentifizierung braucht, sollte das Frontend nicht direkt den privaten Key senden. Stattdessen leitest du die Anfrage über deinen eigenen Server oder eine Middleware weiter, die den Key sicher einfügt.

Kurz gesagt:

  • Alles mit VITE_ ist öffentlich und für den Client sichtbar.
  • Alles ohne VITE_ bleibt intern auf dem Server.
  • Private Tokens gehören nie in den Frontend-Build, sondern in Backend-Logik oder einen Proxy.

Ihre config.ts im src-Ordner

Unter src/config.ts erstellen Sie folgende Datei:

const API_BASE_URL = import.meta.env.VITE_API_BASE_URL as string;
const API_WEBINAR_CONFIG_ENDPOINT = import.meta.env.VITE_API_WEBINAR_CONFIG_ENDPOINT as string;

// Fallback / kleine Sicherheit
if (!API_BASE_URL) {
    console.warn('VITE_API_BASE_URL is not set!');
}

export const API_CONFIG = {
    baseUrl: API_BASE_URL,
    webinarConfigUrl: API_BASE_URL + API_WEBINAR_CONFIG_ENDPOINT,
};

Erklärung

  • import.meta.env
    Vite stellt über dieses Objekt automatisch alle Umgebungsvariablen zur Verfügung, die mit VITE_ beginnen.
    So greifen Sie direkt auf Ihre Werte aus .env.local zu, ohne zusätzliche Imports.
  • const baseUrl / const configEndpoint
    Hier werden die Variablen aus der .env.local geladen.
    Sie können diese später in beliebigen Komponenten oder Services verwenden, indem Sie einfach API_CONFIG.baseUrl oder API_CONFIG.fullConfigUrl importieren.
  • Fallback-Warnung
    Wenn keine Base-URL gesetzt ist, wird eine Warnung in der Konsole ausgegeben.
    Das hilft, Konfigurationsfehler schon in der Entwicklungsphase zu erkennen.
  • Export als Objekt API_CONFIG
    Durch die zentrale Struktur behalten Sie Übersicht und können bei Bedarf weitere Variablen ergänzen (z. B. Auth-Keys, Debug-Modus oder API-Versionen).

Service-Layer: Die zentrale Schnittstelle zu Ihrer API

Damit Ihre Vue-Anwendung übersichtlich bleibt, ist es sinnvoll, API-Aufrufe nicht direkt in den Komponenten zu platzieren, sondern in einer eigenen Schicht zu bündeln – dem Service-Layer.

Dafür legen Sie im Ordner src/services die Datei webinarApi.ts an.

Der Zweck dieser Datei ist:

  • Es gibt eine zentrale Stelle, an der Ihre Anwendung mit der API spricht.
  • Komponenten müssen nicht wissen, wie und wo die Daten geladen werden – sie rufen nur Funktionen wie fetchWebinarConfig() auf.
  • Wenn sich der API-Endpunkt oder die Struktur später ändert, passen Sie es nur hier an und nicht in zig Komponenten.
  • Sie können diese Funktionen später leichter testen und wiederverwenden.

Der Inhalt Ihrer webinarApi.ts sieht aktuell so aus:

import { API_CONFIG } from '@/config';


export type WebinarConfigResponse = {
    response: {
        config: {
            pdlpf_option_webinar_application_startpage: number | null;
        };
        [id: string]: any;
    };
};

export async function fetchWebinarConfig(): Promise<WebinarConfigResponse> {
    const res = await fetch(API_CONFIG.webinarConfigUrl);

    if (!res.ok) {
        throw new Error(`Error loading webinar config: ${res.status}`);
    }

    return res.json();
}

Zeile für Zeile erklärt

import { API_CONFIG } from '@/config';

→ Hier wird das Objekt API_CONFIG aus Ihrer zentralen Konfigurationsdatei src/config.ts importiert.
Dieses Objekt enthält unter anderem die vollständige URL für den Config-Endpunkt (webinarConfigUrl), sodass die Service-Funktion nicht selbst URLs zusammenbauen muss.

export type WebinarConfigResponse = {
    response: {
        config: {
            pdlpf_option_webinar_application_startpage: number | null;
        };
        [id: string]: any;
    };
};
  • export type WebinarConfigResponse = { ... }
    Definiert einen TypeScript-Typ für die Antwort der API und exportiert ihn, damit Sie ihn auch in anderen Dateien verwenden können (z. B. im Pinia-Store).
  • response: { ... }
    Beschreibt das Objekt, das im Feld response der API-Antwort steckt.
    Sie bilden damit die Struktur nach, die Ihre API tatsächlich zurückliefert.
  • config: { pdlpf_option_webinar_application_startpage: number | null; };
    Innerhalb von response gibt es ein Feld config.
    Darin interessiert Sie hier insbesondere die Option
    pdlpf_option_webinar_application_startpage,
    also die ID des Webinars, das als Startseite angezeigt werden soll.
    Der Typ number | null bedeutet:
    • entweder eine numerische ID,
    • oder null, falls nichts gesetzt wurde.
  • [id: string]: any;
    Dies ist eine sogenannte Index-Signatur.
    Sie sagt: Unterhalb von response können noch weitere Felder mit beliebigen Schlüsseln (string) existieren – zum Beispiel die einzelnen Webinare – und diese haben den Typ any.
    Damit geben Sie TypeScript genug Flexibilität, ohne jede mögliche Struktur im Detail typisieren zu müssen.
export async function fetchWebinarConfig(): Promise<WebinarConfigResponse> {

→ Definiert eine asynchrone Funktion fetchWebinarConfig und exportiert sie.

  • async bedeutet, dass in der Funktion await verwendet werden kann und die Funktion ein Promise zurückgibt.
  • Promise<WebinarConfigResponse> gibt an, dass die Funktion ein Promise zurückliefert, das – wenn es erfüllt ist – ein Objekt im Format von WebinarConfigResponse enthält.
    const res = await fetch(API_CONFIG.webinarConfigUrl);

→ Hier wird der eigentliche Netzwerkaufruf ausgeführt:

  • fetch(...) ruft die URL aus API_CONFIG.webinarConfigUrl auf.
    Diese URL stammt aus Ihrer Konfiguration und zeigt auf den REST-Endpunkt Ihrer Webinar-Konfiguration.
  • await wartet, bis die Antwort vom Server da ist, und speichert sie in der Konstanten res.
  • res ist ein Response-Objekt aus der Fetch-API, das Statuscode, Header und Body enthält.
    if (!res.ok) {
        throw new Error(`Fehler beim Laden der Webinar-Config: ${res.status}`);
    }
  • res.ok ist ein Boolean, der true ist, wenn der HTTP-Statuscode im Bereich 200–299 liegt.
  • Mit if (!res.ok) prüfen Sie, ob kein erfolgreicher Status zurückkommt (z. B. 404, 500 etc.).
  • In diesem Fall wird mit throw new Error(...) ein Fehler ausgelöst.
    Dadurch schlagen Aufrufe der Funktion sichtbar fehl, und Sie können im Store oder in der View gezielt auf Fehler reagieren (z. B. Fehlermeldung anzeigen).
    return res.json();
}

→ Wenn die Antwort erfolgreich war, wird hier der Body der Antwort als JSON geparst:

  • res.json() liest den Response-Body und wandelt ihn in ein JavaScript-Objekt um.
  • Durch die Typisierung der Funktion (Promise<WebinarConfigResponse>) gehen Sie davon aus, dass dieses Objekt die Struktur von WebinarConfigResponse hat.
  • return gibt dieses JSON-Objekt zurück – der aufrufende Code (z. B. Ihr Pinia-Store) kann es dann weiterverarbeiten.

Webinar-Store: State-Management mit Pinia strukturieren

Nachdem der Service-Layer steht, folgt der nächste logische Schritt: das State-Management.
Hier geht es darum, die Daten aus Ihrer API zentral zu verwalten, zu speichern und für alle Komponenten verfügbar zu machen.

Dafür erstellen Sie im Ordner src/stores eine neue Datei mit dem Namen webinarStore.ts.
Pinia dient Ihnen dabei als moderne, intuitive State-Management-Lösung für Vue 3.
Der Zweck dieser Datei ist:

  • Alle API-Daten und Zustände (Laden, Fehler, Webinare) zentral an einer Stelle zu halten.
  • Mehrfaches Laden derselben Daten in verschiedenen Komponenten zu vermeiden.
  • Abhängige Werte (z. B. das nächste Webinar) automatisch zu berechnen.
  • Die Logik klar von der Darstellung im Template zu trennen.

Hier ist Ihr aktueller Code:

import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { fetchWebinarConfig, type WebinarConfigResponse } from '@/services/webinarApi';

export const useWebinarStore = defineStore('webinar', () => {
    const isLoading = ref(false);
    const error = ref<string | null>(null);

    // aus der API
    const config = ref<WebinarConfigResponse['response']['config'] | null>(null);
    const webinars = ref<Record<string, any>>({});

    const startWebinarId = computed(() =>
        config.value?.pdlpf_option_webinar_application_startpage ?? null,
    );

    const startWebinar = computed(() => {
        // 1. If a start page is set in the config and we know this webinar → take it
        if (startWebinarId.value && webinars.value[startWebinarId.value]) {
            return webinars.value[startWebinarId.value];
        }

        // 2. Else fallback: first upcoming webinar
        return upcomingWebinars.value[0] ?? null;
    });


    const upcomingWebinars = computed(() => {
        const now = new Date();

        return Object.values(webinars.value)
            .map(webinar => ({
                webinar,
                date: getWebinarDate(webinar),
            }))
            .filter(item => item.date && item.date >= now)
            .sort((a, b) => a.date!.getTime() - b.date!.getTime())
            .map(item => item.webinar);
    });


    function getWebinarDate(webinar: any): Date | null {
        const raw = webinar.pdlpfw_webinar_date;
        if (!raw) return null;

        const d = new Date(raw);
        return isNaN(d.getTime()) ? null : d;
    }


    async function loadWebinarConfig() {
        isLoading.value = true;
        error.value = null;

        try {
            const data = await fetchWebinarConfig();

            // config part
            config.value = data.response.config;

            // store remaining keys as webinars
            const { config: _cfg, ...rest } = data.response;
            webinars.value = rest;
        } catch (e: any) {
            error.value = e?.message ?? 'Error loading webinar config';
        } finally {
            isLoading.value = false;
        }
    }

    return {
        isLoading,
        error,
        config,
        webinars,
        startWebinarId,
        startWebinar,
        upcomingWebinars,
        loadWebinarConfig,
    };

});

Zeile für Zeile erklärt

import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { fetchWebinarConfig, type WebinarConfigResponse } from '@/services/webinarApi';

→ Importiert die benötigten Funktionen und Typen:

  • defineStore: Die Hauptfunktion von Pinia zum Anlegen eines Stores.
  • ref und computed: Reaktive Werkzeuge aus Vue.
  • fetchWebinarConfig: Die Funktion aus dem Service-Layer, die die API aufruft.
  • WebinarConfigResponse: Der Typ der erwarteten Antwort, um die Daten typsicher zu halten.

export const useWebinarStore = defineStore('webinar', () => {

→ Erstellt und exportiert einen Pinia-Store mit dem Namen 'webinar'.
Der Name dient als eindeutige Kennung und wird von Pinia intern genutzt, um den Zustand zu verwalten.
Mit dem Präfix use (z. B. useWebinarStore) folgt die Funktion dem gängigen Vue-Composition-Pattern.


    const isLoading = ref(false);
    const error = ref<string | null>(null);

→ Zwei Basiszustände, um Lade- und Fehlersituationen in der UI darstellen zu können.

  • isLoading signalisiert, ob gerade ein API-Call läuft.
  • error enthält den Fehlermeldungstext oder null, wenn kein Fehler vorliegt.

    const config = ref<WebinarConfigResponse['response']['config'] | null>(null);
    const webinars = ref<Record<string, any>>({});

→ Hier werden die eigentlichen API-Daten abgelegt:

  • config enthält die globale Konfiguration aus der API-Antwort.
  • webinars ist ein Objekt, in dem alle geladenen Webinare nach ID gespeichert werden.

Der Typ Record<string, any> bedeutet: Ein Objekt, dessen Schlüssel Strings (die Webinar-IDs) und deren Werte beliebig typisiert sind.


    const startWebinarId = computed(() =>
        config.value?.pdlpf_option_webinar_application_startpage ?? null,
    );

→ Eine Computed Property, die automatisch die Start-Webinar-ID aus der Config liefert.
Wenn keine ID vorhanden ist, wird null zurückgegeben.
Das ?. ist die optionale Verkettung, um Fehler bei nicht definierten Werten zu vermeiden.

💡 Hinweis zur optionalen Verkettung (?.)

Der Operator ?. ist ein JavaScript-Sprachfeature (kein TypeScript-spezifisches Feature!), das mit ES2020 eingeführt wurde.
Er wird auch in TypeScript unterstützt, weil TypeScript auf modernen JavaScript-Standards basiert.

Der Zweck ist, Zugriffe auf tief verschachtelte Objekte sicherer zu machen,
ohne dass der Code bei einem undefined-Wert abstürzt.

Beispiel ohne ?.

const user = null;
console.log(user.name); // ❌ Fehler: Kann Eigenschaft 'name' von null nicht lesen

In diesem Beispiel bricht der Code ab, weil user kein Objekt ist.

Beispiel mit ?.

const user = null;
console.log(user?.name); // → undefined (kein Fehler)

Der Ausdruck wird hier einfach beendet, sobald der linke Teil (user) null oder undefined ist.
Anstatt einen Fehler zu werfen, liefert er undefined zurück.

Mehrstufige Verkettung

Sie können ?. auch mehrfach hintereinander verwenden:

const user = { profile: { address: { city: 'Berlin' } } };

console.log(user?.profile?.address?.city); // → 'Berlin'
console.log(user?.profile?.contact?.phone); // → undefined, kein Fehler

Ohne ?. würde der zweite Zugriff zu einem Laufzeitfehler führen,
weil user.profile.contact gar nicht existiert.

Warum das im Store nützlich ist

In Ihrem Fall:

config.value?.pdlpf_option_webinar_application_startpage ?? null
  • config.value kann anfangs null sein (bevor die API geladen wurde).
  • Mit ?. fragen Sie sicher ab, ob config.value existiert.
  • Wenn nicht, bricht die Abfrage ab, ohne einen Fehler zu werfen,
    und dank ?? null (Nullish Coalescing Operator) wird einfach null zurückgegeben.

So bleibt Ihr Code stabil, selbst wenn noch keine Daten vorhanden sind.


    const startWebinar = computed(() => {
        if (startWebinarId.value && webinars.value[startWebinarId.value]) {
            return webinars.value[startWebinarId.value];
        }
        return upcomingWebinars.value[0] ?? null;
    });

→ Liefert das tatsächlich aktive Webinar:

  1. Wenn in der Config eine Start-Webinar-ID definiert ist und das Webinar existiert → dieses verwenden.
  2. Falls nicht → Fallback auf das nächste anstehende Webinar aus der upcomingWebinars-Liste.

So bleibt die App immer funktionsfähig, auch wenn keine Start-ID gesetzt ist.


    const upcomingWebinars = computed(() => {
        const now = new Date();

        return Object.values(webinars.value)
            .map(webinar => ({
                webinar,
                date: getWebinarDate(webinar),
            }))
            .filter(item => item.date && item.date >= now)
            .sort((a, b) => a.date!.getTime() - b.date!.getTime())
            .map(item => item.webinar);
    });

→ Eine zweite Computed Property, die automatisch alle zukünftigen Webinare ermittelt:

  • Object.values(webinars.value) → Wandelt das Objekt in ein Array um.
  • map(...) → Liest das Datum jedes Webinars über getWebinarDate() aus.
  • filter(...) → Entfernt alle Webinare, deren Termin in der Vergangenheit liegt.
  • sort(...) → Sortiert die zukünftigen Termine chronologisch.
  • map(...) → Gibt wieder nur die Webinar-Objekte selbst zurück.

So entsteht eine fertige Liste aller kommenden Webinare.


    function getWebinarDate(webinar: any): Date | null {
        const raw = webinar.pdlpfw_webinar_date;
        if (!raw) return null;

        const d = new Date(raw);
        return isNaN(d.getTime()) ? null : d;
    }

→ Eine kleine Hilfsfunktion, die das Datum eines Webinars als echtes Date-Objekt zurückgibt.
Falls das Datum fehlt oder ungültig ist, liefert sie null.
Diese Funktion ist notwendig, um Datumsvergleiche und Sortierungen korrekt durchführen zu können.


    async function loadWebinarConfig() {
        isLoading.value = true;
        error.value = null;

        try {
            const data = await fetchWebinarConfig();
            config.value = data.response.config;

            const { config: _cfg, ...rest } = data.response;
            webinars.value = rest;
        } catch (e: any) {
            error.value = e?.message ?? 'Fehler beim Laden der Webinar-Config';
        } finally {
            isLoading.value = false;
        }
    }

→ Die zentrale Funktion, um die API-Daten zu laden:

  1. Start: isLoading wird auf true gesetzt, error zurückgesetzt.
  2. API-Call: Ruft fetchWebinarConfig() auf, um Daten zu laden.
  3. Erfolg:
    • Speichert den Config-Teil separat.
    • Trennt mit Destrukturierung { config: _cfg, ...rest } die Webinare vom Config-Objekt.
    • Schreibt die restlichen Daten in webinars.
  4. Fehlerfall:
    • Fangt Ausnahmen ab und speichert die Fehlermeldung in error.
  5. Beenden: Setzt isLoading wieder auf false.

    return {
        isLoading,
        error,
        config,
        webinars,
        startWebinarId,
        startWebinar,
        upcomingWebinars,
        loadWebinarConfig,
    };

→ Am Ende werden alle relevanten States, Computed-Properties und Funktionen zurückgegeben.
Diese stehen anschließend in jeder Komponente zur Verfügung, die den Store importiert.

Integration im Frontend: HomeView.vue mit rohen API-Daten

Bevor wir später ein richtiges Layout und Design für die Webinar-Ansicht aufbauen, ist es sinnvoll, die rohen Daten erst einmal direkt im Frontend anzuzeigen.

In diesem Beispiel binden Sie den webinarStore in Ihrer Startseite (z. B. HomeView.vue, Route /) ein und geben einfach alles aus, was aus API → Service → Store kommt. So sehen Sie klar:

  • Kommt die Anfrage an?
  • Werden Config und Webinare korrekt gefüllt?
  • Funktionieren startWebinarId und upcomingWebinars so wie gedacht?

Hier das Beispiel:

<script setup lang="ts">
import { onMounted } from 'vue';
import { useWebinarStore } from '@/stores/webinarStore';

const webinarStore = useWebinarStore();

onMounted(() => {
  webinarStore.loadWebinarConfig();
});
</script>

<template>
  <main>
    <div v-if="webinarStore.isLoading">
      Lade Webinar-Konfiguration...
    </div>

    <div v-else-if="webinarStore.error">
      Fehler: {{ webinarStore.error }}
    </div>

    <section v-else>
      <h1>Aktuelles Webinar</h1>
      <div v-if="webinarStore.config">
        <h1>config</h1>
        <pre>{{ webinarStore.config }}</pre>
      </div>
      <div v-else>
        No config found.
      </div>
      <div v-if="webinarStore.webinars">
        <h1>webinars</h1>
        <pre>{{ webinarStore.webinars }}</pre>
      </div>
      <div v-else>
        No webinars found.
      </div>
      <div v-if="webinarStore.startWebinarId">
        <h1>startWebinarId</h1>
        <pre>{{ webinarStore.startWebinarId }}</pre>
      </div>
      <div v-else>
        No startWebinarId found.
      </div>
      <div v-if="webinarStore.upcomingWebinars">
        <h1>upcomingWebinars</h1>
        <pre>{{ webinarStore.upcomingWebinars }}</pre>
      </div>
      <div v-else>
        No upcomingWebinars found.
      </div>
    </section>
  </main>
</template>

<style scoped>
pre{
  text-align: left;
  font-size: 1.2em;
}
</style>

Zeile für Zeile erklärt

Logik der Komponente

<script setup lang="ts">

→ Sie verwenden den <script setup>-Syntax mit lang="ts".
Das bedeutet:

  • Composition API im Kurzformat
  • TypeScript-Unterstützung direkt in der Komponente

import { onMounted } from 'vue';
import { useWebinarStore } from '@/stores/webinarStore';

→ Zwei Importe:

  • onMounted ist ein Lifecycle-Hook von Vue: Code darin wird ausgeführt, sobald die Komponente im DOM gemountet wurde.
  • useWebinarStore ist Ihre Pinia-Store-Funktion aus webinarStore.ts, über die Sie auf den globalen Webinar-State zugreifen.

const webinarStore = useWebinarStore();

→ Hier initialisieren Sie den Store in dieser Komponente.

  • useWebinarStore() gibt Ihnen eine reaktive Store-Instanz zurück.
  • Über webinarStore greifen Sie im Template auf Felder wie isLoading, error, config, webinars, upcomingWebinars usw. zu.

onMounted(() => {
  webinarStore.loadWebinarConfig();
});

→ Sobald die Komponente geladen ist, wird loadWebinarConfig() aufgerufen.

  • Dadurch startet der API-Call über den Service-Layer.
  • Während des Ladens setzt der Store isLoading auf true, füllt später config und webinars und setzt isLoading wieder auf false.

Damit haben Sie den kompletten Datenfluss:
Komponente → Store → Service → API → Store → Komponente


</script>

→ Ende des Script-Blocks.


<template> – Anzeige der rohen Daten

<template>
  <main>

→ Beginn des Templates.
Das <main>-Element ist der semantische Container für den Hauptinhalt dieser Seite.


    <div v-if="webinarStore.isLoading">
      Lade Webinar-Konfiguration...
    </div>

→ Erste Zustandsabfrage:

  • Solange webinarStore.isLoading === true ist, wird dieser Block angezeigt.
  • Praktisch als „Ladezustand“, um dem Nutzer zu signalisieren, dass Daten geholt werden.

    <div v-else-if="webinarStore.error">
      Fehler: {{ webinarStore.error }}
    </div>

→ Zweiter Zustand: Fehlerfall.

  • Wenn kein Laden mehr aktiv ist, aber webinarStore.error gesetzt wurde, wird diese Meldung ausgegeben.
  • {{ webinarStore.error }} zeigt den Fehlertest an, den Sie im Store im catch-Block gesetzt haben.

    <section v-else>

→ Dritter Zustand:
Nur wenn weder Laden noch Fehler aktiv sind (!isLoading und !error), wird dieser v-else-Block angezeigt.

Hier befinden Sie sich im „Normalzustand“: Daten sind geladen, es gab keinen Fehler.


      <h1>Aktuelles Webinar</h1>

→ Überschrift für diesen Bereich.
Hier könnten später die „echten“ Webinar-Infos (Titel, Datum, Beschreibung) angezeigt werden.
Aktuell steht hier nur ein statischer Platzhalter.


Config ausgeben
      <div v-if="webinarStore.config">
        <h1>config</h1>
        <pre>{{ webinarStore.config }}</pre>
      </div>
      <div v-else>
        No config found.
      </div>
  • v-if="webinarStore.config" prüft, ob die config aus dem Store gesetzt ist.
  • Innerhalb des pre-Tags geben Sie das gesamte Config-Objekt aus – roh, aber ideal zum Debuggen.
  • v-else zeigt „No config found.“, falls config noch null ist oder etwas schiefgelaufen ist.

Webinare ausgeben
      <div v-if="webinarStore.webinars">
        <h1>webinars</h1>
        <pre>{{ webinarStore.webinars }}</pre>
      </div>
      <div v-else>
        No webinars found.
      </div>
  • webinarStore.webinars ist das Objekt, das alle Webinare enthält (nach IDs).
  • Im pre-Block sehen Sie die rohe Struktur, so wie sie aus der API übernommen wurde.

💡 Hinweis: In Ihrem aktuellen Store ist webinars standardmäßig ein leeres Objekt ({}).
Ein leeres Objekt ist in JavaScript truthy, das heißt:
v-if="webinarStore.webinars" ist immer „wahr“, auch wenn noch keine Webinare drin sind.
Wenn Sie wirklich „keine Webinare“ erkennen möchten, könnten Sie später prüfen, ob Object.keys(webinarStore.webinars).length === 0 ist.
Für Debug-Zwecke ist die aktuelle Variante aber völlig ausreichend.


Start-Webinar-ID anzeigen
      <div v-if="webinarStore.startWebinarId">
        <h1>startWebinarId</h1>
        <pre>{{ webinarStore.startWebinarId }}</pre>
      </div>
      <div v-else>
        No startWebinarId found.
      </div>
  • Zeigt an, ob in der Config eine Start-Webinar-ID gesetzt ist.
  • Wenn eine ID vorhanden ist, wird sie angezeigt.
  • Im else-Fall sehen Sie, dass aktuell keine Startseite definiert ist.

Liste der kommenden Webinare anzeigen
      <div v-if="webinarStore.upcomingWebinars">
        <h1>upcomingWebinars</h1>
        <pre>{{ webinarStore.upcomingWebinars }}</pre>
      </div>
      <div v-else>
        No upcomingWebinars found.
      </div>
  • upcomingWebinars ist ein Array, das die kommenden Webinare enthält, sortiert nach Datum.
  • Im pre-Block sehen Sie genau, welche Einträge nach Ihrer Filter- & Sortierlogik als „zukünftig“ erkannt wurden.

Auch hier gilt: Ein leeres Array [] ist in JavaScript truthy, das heißt v-if="webinarStore.upcomingWebinars" ist immer „wahr“.
Wenn Sie später „keine Ergebnisse“ sauber behandeln möchten, könnten Sie auf webinarStore.upcomingWebinars.length prüfen.


    </section>
  </main>
</template>

→ Ende des Templates.


<style scoped> – minimale Darstellung

<style scoped>
pre{
  text-align: left;
  font-size: 1.2em;
}
</style>
  • scoped sorgt dafür, dass diese Styles nur in dieser Komponente gelten.
  • Sie formatieren hier lediglich den pre-Block minimal, damit die rohen Daten lesbarer sind:
    • Linksbündig
    • etwas größere Schrift

Damit bleibt der Fokus ganz auf der Funktionskontrolle, nicht auf Design.

Architekturprinzip: Trennung von Verantwortung

Ein zentrales Prinzip moderner Frontend-Architekturen – und ganz besonders in Vue 3 mit der Composition API – ist die klare Trennung von Verantwortung (Separation of Concerns).
Durch diese Struktur bleibt Ihre Anwendung übersichtlich, testbar und langfristig wartbar.

1. Service – die Datenquelle

Der Service-Layer (z. B. webinarApi.ts) kümmert sich ausschließlich um die Kommunikation mit externen Schnittstellen:
Er ruft Daten von der API ab, wandelt sie bei Bedarf leicht um und gibt sie als Promise an den Store weiter.

👉 Der Service enthält keine UI-Logik, keine Zustände und keine Berechnungen.
Er ist die Brücke zwischen Backend und Frontend, also die Stelle, an der Ihre Anwendung wirklich mit der Außenwelt spricht.

Vorteil:
Wenn sich das Backend ändert (z. B. neue URL, neue API-Struktur), passen Sie den Code nur im Service an – der Rest der App bleibt unverändert.

2. Store – die zentrale Datenverwaltung

Der Store (z. B. webinarStore.ts) bildet die Datenzentrale Ihrer Anwendung.
Er speichert, was die API liefert, und stellt es allen Komponenten reaktiv zur Verfügung.

Hier werden Zustände wie isLoading, error, config oder webinars gehalten.
Außerdem enthält der Store berechnete Werte (computed properties) wie startWebinar oder upcomingWebinars, die aus den Rohdaten abgeleitet werden.

Vorteil:

  • Nur eine einzige Quelle der Wahrheit („Single Source of Truth“)
  • Keine doppelten Daten in mehreren Komponenten
  • Saubere Trennung zwischen Datenlogik und Darstellung

3. View / Komponenten – die Präsentationsschicht

Die View– oder Komponentenebene (z. B. HomeView.vue) ist ausschließlich dafür da,
die vom Store bereitgestellten Daten anzuzeigen oder mit Benutzerinteraktionen reagierend umzugehen.

Hier befindet sich:

  • das Template (HTML / Vue-Template-Syntax),
  • gegebenenfalls einfache Logik für Anzeige oder Formatierung,
  • aber keine API-Aufrufe und keine globale State-Logik.

Vorteil:
Komponenten bleiben schlank, wiederverwendbar und leicht testbar.
Sie konzentrieren sich nur darauf, was angezeigt wird, nicht woher die Daten stammen.

Zusammenspiel

So sieht der Datenfluss in Ihrer Architektur aus:

Backend (API)
     ↓
Service (Daten abrufen)
     ↓
Store (Daten speichern & verarbeiten)
     ↓
View / Komponenten (Daten anzeigen)
  • Der Service ruft Daten ab.
  • Der Store verwaltet und strukturiert sie.
  • Die View zeigt sie an.

Jede Schicht hat genau eine Aufgabe – dadurch vermeiden Sie Chaos, doppelte Logik und schwer nachvollziehbare Abhängigkeiten.

Mit dieser Trennung schaffen Sie die Grundlage für eine skalierbare, modulare und stabile Anwendung.
Neue API-Endpunkte, zusätzliche Komponenten oder komplexere Zustände lassen sich problemlos ergänzen,
ohne dass Sie bestehende Logik brechen.

Dieser Aufbau – Service → Store → View – ist das Rückgrat Ihrer gesamten Webinar-App und
bildet die Basis für alle kommenden Schritte wie Formularintegration, Benutzerverwaltung oder dynamische Seiten in den nächsten Teilen Ihrer Serie.