CRUD-App: DevNotes
Alles zusammengesetzt: Laden, Anlegen, Bearbeiten und Löschen von Notizen — die echte App dieses Kurses.
Die App, die du gerade benutzt
DevNotes ist eine vollständige CRUD-App, gebaut aus genau den Bausteinen
der letzten Module: Load-Funktionen zum Anzeigen, Form Actions zum Verändern, Hooks für die Anmeldung und Drizzle/Postgres als Speicher. Kein einziges manuelles fetch.
🛤️ Rails-Vergleich: der CRUD-Controller
Die Struktur ist dir vertraut: index lädt, create/destroy verändern. In SvelteKit ist load das index, und benannte Actions
sind create/update/destroy.
# Rails: der klassische CRUD-Controller
class NotesController < ApplicationController
def index; @notes = current_user.notes.order(updated_at: :desc); end
def create; current_user.notes.create!(note_params); redirect_to notes_path; end
def destroy; current_user.notes.find(params[:id]).destroy; end
end1. Laden (Read)
Die load-Funktion holt die Notizen des angemeldeten Users:
import { desc, eq } from 'drizzle-orm';
import { db, schema } from '$lib/server/db';
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ locals }) => {
if (!locals.user) throw redirect(303, '/login'); // Guard (before_action)
const notes = await db.select().from(schema.notes)
.where(eq(schema.notes.userId, locals.user.id))
.orderBy(desc(schema.notes.updatedAt));
return { user: locals.user, notes }; // landet in data-Prop
};2. Verändern (Create / Delete)
Benannte Form Actions in derselben Datei kümmern sich um Mutationen:
import { fail, redirect } from '@sveltejs/kit';
import { and, eq } from 'drizzle-orm';
export const actions = {
create: async ({ request, locals }) => {
if (!locals.user) throw redirect(303, '/login');
const data = await request.formData();
const title = String(data.get('title') ?? '').trim();
if (!title) return fail(400, { error: 'Titel darf nicht leer sein.' });
await db.insert(schema.notes).values({ userId: locals.user.id, title });
return { success: true };
},
delete: async ({ request, locals }) => {
const id = Number((await request.formData()).get('id'));
await db.delete(schema.notes)
.where(and(eq(schema.notes.id, id), eq(schema.notes.userId, locals.user!.id)));
return { success: true };
}
};3. Die Oberfläche
Die Seite bekommt data (aus load) und form (aus der Action) als Props. use:enhance macht die Formulare zu SPA-Interaktionen — funktioniert aber auch ohne
JavaScript:
<script lang="ts">
import { enhance } from '$app/forms';
let { data, form } = $props(); // data.notes aus load, form aus der Action
</script>
<form method="POST" action="?/create" use:enhance>
<input name="title" placeholder="Neue Notiz…" required />
<button>Anlegen</button>
</form>
{#each data.notes as note (note.id)}
<li>
<a href="/devnotes/{note.id}">{note.title}</a>
<form method="POST" action="?/delete" use:enhance>
<input type="hidden" name="id" value={note.id} />
<button>Löschen</button>
</form>
</li>
{/each}💡 Progressive Enhancement
Ohne use:enhance senden die Formulare einen echten POST und die Seite lädt neu —
die App funktioniert also auch ganz ohne JS. Mit use:enhance wird derselbe
Ablauf ohne Reload abgewickelt. Das Beste aus beiden Welten.
Bearbeiten unter /devnotes/[id]
Die Detail-/Bearbeiten-Route nutzt eine dynamische [id]-Route (Modul 8) mit eigener load (Notiz holen, Besitz prüfen) und einer update-Action.
Zusammenfassung
💡 Das Wichtigste
- Eine CRUD-Ressource =
load(Read) + benannte Actions (Create/Update/Delete). - Besitz-Prüfung gehört in die Query (
WHERE user_id = …). use:enhance= SPA-Gefühl mit voller No-JS-Fallback-Sicherheit.
🎯 Probier es selbst
Öffne DevNotes, melde dich an (oder registriere dich) und lege eine Notiz an. Schau dir dann im Code an, welche Action dabei ausgelöst wird.