Use Cases (Aktualisiert: 1.6.2026)

Sichere Datei-Uploads mit Claude Code: FormData, Validierung, Vorschau und S3

Sichere SaaS-Datei-Uploads mit Claude Code: File API, FormData, fetch, Validierung, Fortschritt und S3.

Sichere Datei-Uploads mit Claude Code: FormData, Validierung, Vorschau und S3

Ein Datei-Upload wirkt klein, bis er in einem echten SaaS-Produkt läuft. Profilbild, PDF-Rechnung, CSV-Import, Vertrag oder Chat-Anhang beginnen alle mit einem freundlichen Button. Hinter diesem Button liegen aber Browser-APIs, Request-Format, Servervalidierung, Storage-Berechtigungen, öffentliche oder private Auslieferung, Vorschau, Fortschritt, Kosten und Audit-Logs.

Claude Code kann diese Arbeit stark beschleunigen. Es kann React-Komponente, API-Route, Validierungshelfer, Storage-Adapter und README erzeugen. Der Prompt braucht aber klare Grenzen. Wenn du nur “baue Datei-Upload” schreibst, bekommst du leicht eine Demo, die läuft, aber Originaldateinamen speichert, dem MIME-Typ aus dem Browser vertraut, keine Größenbegrenzung hat oder zu früh eine öffentliche URL zurückgibt.

Dieser Artikel geht in einer prüfbaren Reihenfolge vor. Zuerst trennen wir File API, FormData und Fetch API. Danach bauen wir einen minimalen Upload, React-Vorschau mit echtem Fortschritt, Servervalidierung und die Entscheidung, wann S3, Cloud Storage, Azure Blob Storage oder Cloudflare R2 sinnvoll sind.

Für S3 lies zusätzlich Claude Code und AWS S3. Für die Sicherheitsprüfung passt Claude Code Security Best Practices.

Browser-Aufgaben sauber trennen

Die erste Aufgabe ist die File API. Wenn Nutzer über<input type="file">eine Datei auswählen oder eine Datei in die Seite ziehen, erhältst du einFileObjekt. Daraus liest dufile.name, file.size, file.type und file.lastModified. Für Bilder kannst du mitURL.createObjectURL(file)eine temporäre Vorschau-URL erzeugen.

Die zweite Aufgabe ist FormData. FormData sendet Felder und Dateien alsmultipart/form-data. Üblich istformData.append("file", file). Wichtig: Wenn du FormData mit fetch sendest, setze denContent-TypeHeader nicht manuell. Der Browser ergänzt die multipart boundary. Ein hart codierter Header kann den Request kaputt machen.

Die dritte Aufgabe ist fetch. Für einfache Uploads reicht das:

const formData = new FormData();
formData.append("file", file);

await fetch("/api/upload", {
  method: "POST",
  body: formData
});

Der Sonderfall ist Fortschritt. Fetch ist gut für den Request, aber Upload-Fortschritt ist mitXMLHttpRequest.upload.onprogress weiterhin direkter. Deshalb bitte ich Claude Code explizit: fetch für den einfachen Pfad, XMLHttpRequest für echte Progress Bars.

Minimaler HTML- und fetch-Upload

Starte ohne React und ohne S3. Prüfe zuerst Datei auswählen, Browser-Validierung, Vorschau und FormData-Versand.

<form id="upload-form">
  <input id="file-input" name="file" type="file" accept="image/png,image/jpeg,application/pdf" />
  <button type="submit">Hochladen</button>
</form>
<img id="preview" alt="" style="max-width: 240px; display: none;" />
<p id="message"></p>

<script type="module">
  const MAX_BYTES = 5 * 1024 * 1024;
  const allowedTypes = new Set(["image/png", "image/jpeg", "application/pdf"]);
  const form = document.querySelector("#upload-form");
  const input = document.querySelector("#file-input");
  const preview = document.querySelector("#preview");
  const message = document.querySelector("#message");

  input.addEventListener("change", () => {
    const file = input.files?.[0];
    preview.style.display = "none";
    preview.removeAttribute("src");
    message.textContent = "";

    if (!file) return;
    if (!allowedTypes.has(file.type)) {
      message.textContent = "Nur PNG, JPEG und PDF sind erlaubt.";
      input.value = "";
      return;
    }
    if (file.size > MAX_BYTES) {
      message.textContent = "Die Datei darf maximal 5MB groß sein.";
      input.value = "";
      return;
    }
    if (file.type.startsWith("image/")) {
      preview.src = URL.createObjectURL(file);
      preview.style.display = "block";
      preview.onload = () => URL.revokeObjectURL(preview.src);
    }
  });

  form.addEventListener("submit", async (event) => {
    event.preventDefault();
    const file = input.files?.[0];
    if (!file) {
      message.textContent = "Bitte zuerst eine Datei auswählen.";
      return;
    }

    const formData = new FormData();
    formData.append("file", file);

    const response = await fetch("/api/upload", {
      method: "POST",
      body: formData
    });

    const result = await response.json();
    message.textContent = response.ok ? `Gespeichert: ${result.name}` : result.error;
  });
</script>

Client-Validierung ist UX, keine Sicherheit. Ein Angreifer kann dieses JavaScript umgehen. Der Server muss MIME-Typ, Endung, Größe, Speichername und Speicherort erneut prüfen.

React mit Vorschau und echtem Fortschritt

In React sollten die Zustände getrennt bleiben: ausgewählte Datei, Vorschau-URL, Fortschritt, Fehler und gespeicherter Name. So kann Claude Code später Drag and Drop, Abbrechen oder Retry sauber ergänzen.

import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from "react";

const MAX_BYTES = 5 * 1024 * 1024;
const ALLOWED_TYPES = new Set(["image/png", "image/jpeg", "application/pdf"]);

type UploadResult = { ok: true; name: string; size: number; type: string };

export function FileUploadBox() {
  const [selectedFile, setSelectedFile] = useState<File | null>(null);
  const [previewUrl, setPreviewUrl] = useState<string | null>(null);
  const [progress, setProgress] = useState(0);
  const [error, setError] = useState<string | null>(null);
  const [uploadedName, setUploadedName] = useState<string | null>(null);
  const canUpload = useMemo(() => selectedFile && !error, [selectedFile, error]);

  useEffect(() => {
    return () => {
      if (previewUrl) URL.revokeObjectURL(previewUrl);
    };
  }, [previewUrl]);

  function handleFileChange(event: ChangeEvent<HTMLInputElement>) {
    const file = event.target.files?.[0] ?? null;
    setUploadedName(null);
    setProgress(0);
    setError(null);
    if (previewUrl) URL.revokeObjectURL(previewUrl);
    setPreviewUrl(null);

    if (!file) return setSelectedFile(null);
    if (!ALLOWED_TYPES.has(file.type)) {
      setSelectedFile(null);
      return setError("Nur PNG, JPEG und PDF sind erlaubt.");
    }
    if (file.size > MAX_BYTES) {
      setSelectedFile(null);
      return setError("Die Datei darf maximal 5MB groß sein.");
    }
    setSelectedFile(file);
    if (file.type.startsWith("image/")) setPreviewUrl(URL.createObjectURL(file));
  }

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    if (!selectedFile) return;
    const formData = new FormData();
    formData.append("file", selectedFile);
    const result = await uploadWithProgress(formData, setProgress);
    setUploadedName(result.name);
  }

  return (
    <form onSubmit={handleSubmit} className="space-y-4">
      <input type="file" accept="image/png,image/jpeg,application/pdf" onChange={handleFileChange} />
      {previewUrl && <img src={previewUrl} alt="Vorschau der ausgewählten Datei" width={240} />}
      {selectedFile && <p>{selectedFile.name} / {Math.round(selectedFile.size / 1024)}KB</p>}
      {error && <p role="alert">{error}</p>}
      <progress value={progress} max={100}>{progress}%</progress>
      <button type="submit" disabled={!canUpload}>Hochladen</button>
      {uploadedName && <p>Gespeichert: {uploadedName}</p>}
    </form>
  );
}

function uploadWithProgress(formData: FormData, onProgress: (progress: number) => void) {
  return new Promise<UploadResult>((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open("POST", "/api/upload");
    xhr.upload.addEventListener("progress", (event) => {
      if (event.lengthComputable) onProgress(Math.round((event.loaded / event.total) * 100));
    });
    xhr.addEventListener("load", () => {
      const body = JSON.parse(xhr.responseText || "{}");
      if (xhr.status >= 200 && xhr.status < 300) resolve(body);
      else reject(new Error(body.error ?? "Upload failed"));
    });
    xhr.addEventListener("error", () => reject(new Error("Network error")));
    xhr.send(formData);
  });
}

Der Punkt ist Ehrlichkeit. Wenn du keinen echten Fortschritt misst, zeige einfach “wird hochgeladen”. Eine simulierte Progress Bar sieht in einer Demo gut aus, ist bei schlechter Verbindung aber irreführend.

Servervalidierung, die man prüfen kann

Diese Next.js Route speichert zuerst lokal in.local-uploads. In Produktion ersetzt du diesen Teil durch S3, Cloud Storage, Azure Blob Storage oder R2.

// app/api/upload/route.ts
import { randomUUID } from "node:crypto";
import { mkdir, writeFile } from "node:fs/promises";
import path from "node:path";
import { NextRequest, NextResponse } from "next/server";

export const runtime = "nodejs";

const MAX_BYTES = 5 * 1024 * 1024;
const ALLOWED_TYPES = new Map([
  ["image/png", ".png"],
  ["image/jpeg", ".jpg"],
  ["application/pdf", ".pdf"]
]);

export async function POST(request: NextRequest) {
  const formData = await request.formData();
  const value = formData.get("file");
  if (!(value instanceof File)) {
    return NextResponse.json({ error: "Datei fehlt." }, { status: 400 });
  }

  const expectedExt = ALLOWED_TYPES.get(value.type);
  const originalExt = path.extname(value.name).toLowerCase();
  if (!expectedExt) return NextResponse.json({ error: "MIME-Typ ist nicht erlaubt." }, { status: 400 });
  if (value.size === 0 || value.size > MAX_BYTES) {
    return NextResponse.json({ error: "Die Datei muss zwischen 1 Byte und 5MB groß sein." }, { status: 400 });
  }
  if (expectedExt === ".jpg" && ![".jpg", ".jpeg"].includes(originalExt)) {
    return NextResponse.json({ error: "JPEG-Endung ist ungültig." }, { status: 400 });
  }
  if (expectedExt !== ".jpg" && originalExt !== expectedExt) {
    return NextResponse.json({ error: "MIME-Typ und Endung passen nicht zusammen." }, { status: 400 });
  }

  const bytes = Buffer.from(await value.arrayBuffer());
  const storedName = `${randomUUID()}${expectedExt === ".jpg" ? ".jpg" : expectedExt}`;
  const uploadDir = path.join(process.cwd(), ".local-uploads");
  await mkdir(uploadDir, { recursive: true });
  await writeFile(path.join(uploadDir, storedName), bytes, { flag: "wx" });
  return NextResponse.json({ ok: true, name: storedName, size: value.size, type: value.type });
}

Prüfe mindestens MIME-Typ, Dateiendung, Größe, Speichername und Speicherort. Für sensible Dokumente brauchst du zusätzlich Magic Number, echtes Decoding, Virenscan, Authentifizierung, Quoten und Audit-Logs.

Wann S3 oder Cloud Storage sinnvoll ist

Lokaler Speicher ist gut für Lernen und Prototypen. Für Kundendateien, mehrere Server, Lifecycle-Regeln, private Downloads oder asynchrone Bildverarbeitung brauchst du Object Storage.

Du musst nicht am ersten Tag direkt aus dem Browser nach S3 hochladen. Bei kleinen Dateien kann der App-Server empfangen, validieren und dann speichern. Presigned URLs werden wichtig, wenn Dateigröße, Traffic oder Serverbandbreite wirklich zum Problem werden.

Stelle den bestehenden lokalen Next.js Datei-Upload auf S3 Storage um.
Aktuell: /api/upload empfängt FormData und validiert MIME, Endung und Größe.
Regeln: Den Originalnamen nicht als S3 key verwenden. Speichern als uploads/yyyy/mm/dd/{uuid}.ext.
Regeln: Nur image/png, image/jpeg und application/pdf erlauben. Maximal 5MB.
Regeln: Bucket bleibt private. Die API gibt eine Datei-ID zurück, keine öffentliche URL.
Ergebnisse: app/api/upload/route.ts, lib/storage/s3.ts, Tests für Fehlerfälle, README mit Env Vars.
Prüfung: Tests für zu große Datei, falsche Endung, fehlende Anmeldung und erfolgreichen Upload erklären.

Beispiele, Fehler und Prompt

Beispiel 1 ist das Profilbild. Vorschau, Zuschnitt und Retry zählen. Starte mit PNG, JPEG und WebP; SVG nur nach bewusster Prüfung.

Beispiel 2 ist CSV-Import. Der Upload ist nur der Eingang. Wichtig sind Spalten, Encoding, Zeilenanzahl, Duplikate, Rollback und Fehlerbericht.

Beispiel 3 sind Rechnungen und Verträge als PDF. Gib keine öffentliche URL zurück. Speichere privat, prüfe Organisation und Nutzer, und gib nur eine kurzlebige signierte URL aus.

Häufige Fehler: accept als Sicherheit missverstehen, Originalnamen speichern, direkt eine öffentliche URL zurückgeben, Fortschritt fälschen, S3-Rechte zu breit setzen.

Füge dieser Next.js App einen sicheren Datei-Upload hinzu.
Ziel: SaaS Admin-Seite, die jeweils eine PNG/JPEG/PDF-Datei hochlädt.
Client: React-Komponente mit File API; Dateiname, Größe, Bildvorschau, Fehler und Upload-Status anzeigen.
Transport: FormData per POST an /api/upload. Wenn echter Fortschritt nötig ist, XMLHttpRequest verwenden und erklären, warum fetch nicht reicht.
Server: In app/api/upload/route.ts FormData empfangen und MIME, Endung, 5MB Limit und leere Datei validieren.
Storage: Originalnamen nicht als Speichername verwenden. UUID + Endung unter .local-uploads speichern.
Verboten: Nicht direkt in public/ speichern. Nicht behaupten, Endungsprüfung sei vollständige Sicherheit. S3 Bucket nicht public machen.
Prüfung: Tests für zu große Datei, falsche Endung, fehlende Datei und erfolgreichen Upload erklären.
Referenzen: MDN File API, FormData und Fetch API.

Masa-Verifikationsnotiz

In meinen Tests wurde das Ergebnis deutlich besser, sobald ich “einfacher Upload mit fetch” und “Upload mit echtem Fortschritt” getrennt habe. Ohne diese Trennung erzeugt Claude Code manchmal eine schöne Progress Bar, die nicht an echten Netzwerkfortschritt gekoppelt ist.

Auch der Start mit lokalem Speicher war hilfreich. Erst File API, FormData, Serverroute und Vorschau stabilisieren, dann den Storage-Adapter austauschen. So kommen IAM, CORS und signierte URLs erst dazu, wenn die Basis klar ist.

Zusammenfassung

Ein sicherer Upload ist keine einzelne Eingabe, sondern eine Grenze zwischen Nutzergerät, App-Server, Storage und Berechtigungsmodell. Claude Code kann sie schnell bauen, wenn der Prompt File API, FormData, Servervalidierung, Größenlimit, generierte Speicherkeys, ehrlichen Fortschritt und privaten Object Storage klar nennt.

Wenn du dieses Muster auf ein echtes Repository anwenden möchtest, kann Claude Code Training und Beratung Upload-Flows, S3, signierte URLs, Tests und Review-Checklisten gemeinsam abdecken. Kostenlose PDFs und Lernmaterialien sind ein guter erster Schritt.

#Claude Code #file upload #FormData #S3 #React #security
Kostenlos

Kostenloses PDF: Claude-Code-Cheatsheet

E-Mail eintragen und eine Seite mit Befehlen, Review-Gewohnheiten und sicheren Workflows herunterladen.

Wir schützen Ihre Daten und senden keinen Spam.

Masa

Über den Autor

Masa

Engineer für praktische Claude-Code-Workflows und Team-Einführung.