Frontend-API
Grundprinzip
Abschnitt betitelt „Grundprinzip“Svelte-Komponenten rufen niemals fetch() direkt auf. Stattdessen
gibt es pro Backend-Ressource eine Service-Klasse in frontend/src/api/,
gebündelt im zentralen api-Objekt:
<script> import { api, ApiError } from '../api';
let tenants = $state([]);
async function load() { tenants = await api.tenants.getAll(); }</script>Verfügbar: api.auth, api.users, api.apiKeys, api.tenants
(inkl. Submissions/Quarantäne/Stats), api.routes (SMTP-Routen,
Test-Versand), api.blocklist, api.system (Version/Build) sowie
api.client (reaktiver Zustand).
Was der ApiClient zentral erledigt
Abschnitt betitelt „Was der ApiClient zentral erledigt“frontend/src/api/client.svelte.js ist die einzige Stelle mit fetch:
- JWT automatisch anhängen: Ist eine Session aktiv, bekommt jeder
Request den
Authorization: Bearer …-Header. - Fehler normalisieren: Jede Fehlerantwort wird zu einer
ApiErrormitstatusundmessage(aus dem{"error": "..."}-Body). - Auto-Logout bei 401: Session verwerfen + Redirect auf
/login— genau einmal implementiert, gilt überall. - Session-Persistenz: Token + Profil in
localStorage; ein Reload behält den Login.
Der Auth-Zustand ist ein $state-Feld in einer .svelte.js-Datei —
api.client.isLoggedIn, api.client.user und hasPermission() sind
dadurch reaktiv.
Der auth-Store
Abschnitt betitelt „Der auth-Store“Für Komponenten gibt es den schlanken Wrapper
frontend/src/stores/auth.svelte.js:
<script> import { auth } from '../stores/auth.svelte.js';</script>
{#if auth.isLoggedIn} Hallo, {auth.user.display_name}!{/if}
{#if auth.can('tenants:write')} <button>Neuer Mandant</button>{/if}auth.can() ist reine UI-Kosmetik (Buttons ausblenden) — die
verbindliche Prüfung macht immer der Server per RBAC-Middleware.
Eine neue API-Klasse anlegen
Abschnitt betitelt „Eine neue API-Klasse anlegen“1. Klasse — frontend/src/api/xyz.js (Vorlage: routes.js):
export class XyzApi { #client; constructor(client) { this.#client = client; }
getAll() { return this.#client.get('/xyz'); } create(input) { return this.#client.post('/xyz', input); } remove(id) { return this.#client.delete(`/xyz/${id}`); }}2. Registrieren — in frontend/src/api/index.js importieren und ans
api-Objekt hängen.
3. Verwenden — await api.xyz.getAll() in jeder Komponente.
Svelte-5-Patterns
Abschnitt betitelt „Svelte-5-Patterns“Lokaler Zustand + Laden (Vorlage: pages/Tenants.svelte):
<script> let tenants = $state([]); let error = $state('');
async function load() { try { tenants = await api.tenants.getAll(); error = ''; } catch (e) { error = e instanceof ApiError ? e.message : String(e); } }
$effect(() => { load(); });</script>Abhängigkeitsgetriggertes Neuladen (Vorlage: pages/TenantDetail.svelte
— Statusfilter):
$effect(() => { statusFilter; // getrackte Abhängigkeit: Filterwechsel lädt neu load();});Props (svelte-spa-router übergibt Routen-Parameter):
<script> let { params = {} } = $props(); // params.id aus '/tenants/:id'</script>Fehlerbehandlung: immer ApiError abfangen und e.message
anzeigen — so wird ein 403 („fehlende Berechtigung: tenants:write”) zur
verständlichen UI-Meldung statt zu einer Konsolen-Exception.
Routing
Abschnitt betitelt „Routing“svelte-spa-router mit Hash-Routing (/#/tenants): funktioniert im
Single-Binary ohne Server-Konfiguration. Routen sind in App.svelte
definiert; neue Seite = neue Datei in pages/ + Eintrag im
routes-Objekt. Links mit use:link, programmatisch mit push('/pfad').
Dev-Workflow
Abschnitt betitelt „Dev-Workflow“make devstartet das Go-Backend auf :8080 und Vite auf :5173 (mit
/api-Proxy aufs Backend). Frontend-Änderungen erscheinen per
Hot-Reload sofort; fürs finale Testen make build und das Binary
starten. Die Public API (/v1) testet man am einfachsten mit der
Beispielseite client/example.html gegen das
laufende Binary (Origin des Testservers beim Mandanten eintragen).