Datei-Uploads und Speicherung in einer KI-gebauten App handhaben
Kurzantwort
Lass eine Datei niemals über deinen eigenen Server laufen. Das richtige Muster ist eine signierte URL: dein Backend bittet den Storage-Provider um eine kurzlebige Erlaubnis, der Browser lädt direkt zu diesem Provider hoch, und dein Server sieht am Ende nur den finalen Pfad und die Metadaten der Datei. Machst du es anders, hast du dir einen langsamen, speicherhungrigen Upload-Endpunkt gebaut, der beim ersten Versuch, ein 200-MB-Video zu senden, in die Knie geht.
Warum "die Datei einfach auf dem eigenen Server entgegennehmen" scheitert
Der erste Instinkt der meisten Leute ist der einfachste: ein Formularfeld hinzufügen, an eine API-Route posten, die Bytes auf die Festplatte oder in eine Datenbankspalte schreiben. In der Demo funktioniert das. Profilbild hochladen, es rendern sehen, ausliefern.
Dann lädt jemand ein 40-MB-PDF hoch. Deine Serverless-Function hat ein Payload-Limit --- oft 4,5 MB, manchmal weniger --- und der Request schlägt einfach fehl, ohne brauchbare Fehlermeldung, nur ein Timeout oder ein 413. Oder es klappt, aber jetzt hält dein Serverprozess die komplette Datei im Speicher, während er sie woanders hin weiterlädt --- für einen Nutzer kein Problem, für fünfzig gleichzeitige ein echtes.
Ich habe Teams stundenlang debuggen sehen in der Annahme, es sei ein Bug im eigenen Code. Ist es nicht. Es ist ein Architekturproblem: Der Server sollte nie in der Mitte dieser Übertragung stehen.
Das Anzeichen, dass du es falsch machst
Wenn die Aufgabe deiner API-Route die Wörter "empfangen" und "weiterleiten" im selben Satz enthält --- die Datei empfangen, an den Storage weiterleiten ---, hast du einen Hop eingebaut, der nicht existieren müsste, und einen Flaschenhals, der mit deinem Traffic skaliert statt mit dem deines Storage-Providers.
Das Muster, das tatsächlich skaliert
Ein funktionierender Upload-Flow hat drei Schritte, und keiner davon leitet die Datei über deinen Anwendungsserver.
Schritt eins --- signierte URL anfordern. Der Browser sagt deinem Backend: "Ich möchte eine Datei namens invoice.pdf hochladen, ein PDF, ungefähr 2 MB." Dein Backend prüft, ob dieser Nutzer hier überhaupt hochladen darf, und fragt dann den Storage-Provider (S3, Supabase Storage, Cloudflare R2 --- was auch immer du nutzt) nach einer vorsignierten URL, die exakt auf diese Datei begrenzt ist und ein paar Minuten gültig bleibt.
Schritt zwei --- direkt hochladen. Der Browser nutzt diese URL, um die Datei per PUT direkt zum Storage zu senden. Dein Server wird während dieses Teils überhaupt nicht berührt. Ein 500-MB-Video verbraucht dieselben Serverressourcen wie ein 5-KB-Thumbnail: null.
Schritt drei --- bestätigen und speichern. Sobald der Upload fertig ist, teilt der Browser (oder ein Storage-Webhook, falls der Provider das unterstützt) deinem Backend mit: "fertig, hier ist der finale Pfad." Dein Backend schreibt diesen Pfad in die Datenbank, führt jede Validierung durch, die vorher nicht möglich war, und die Datei ist jetzt Teil der Daten deiner App.
Das ist dieselbe Form, egal ob du Nutzer-Avatare speicherst oder Massen-CSV-Importe handhabst --- nur der Validierungsschritt ändert sich.
Was "Validierung, die vorher nicht möglich war" tatsächlich bedeutet
Eine signierte URL legt Dateiname und ungefähre Größe fest, kann aber keinen Inhalt prüfen, den sie noch nicht gesehen hat. Genau hier zählt eine zweite Ebene: Nach dem Upload den tatsächlichen Dateityp anhand der Bytes prüfen (nicht der Endung, die ein Nutzer eingegeben hat), alles ablehnen, was nicht mit der Angabe übereinstimmt, und den Gesamtspeicher pro Konto begrenzen, damit nicht ein Nutzer heimlich deinen Bucket füllt. Nichts davon ist exotisch --- es sind ein paar Zeilen im Bestätigungsschritt ---, aber es zu überspringen ist der Grund, warum Apps am Ende eine .exe ausliefern, die in .jpg umbenannt wurde.
Ein Vergleich: proxied Uploads vs. Direct-to-Storage
| Aspekt | Server-proxied Upload | Direct-to-Storage (signierte URL) |
|---|---|---|
| Maximal praktikable Dateigröße | Begrenzt durch Function-Payload-Caps (oft ~4,5 MB) | Nur begrenzt durch den Storage-Provider (mehrere GB) |
| Serverlast beim Upload | Skaliert mit gleichzeitigen Uploads | Praktisch null --- Server ist nicht im Übertragungspfad |
| Time to First Byte bei großen Dateien | Langsam --- Server puffert, dann leitet weiter | Schnell --- direkte Verbindung zum CDN-Edge des Storage |
| Aufwand zum Einrichten | Am Anfang niedriger (ein Endpunkt) | Etwas mehr (Request für signierte URL + Bestätigungsschritt) |
| Wo es bricht | Sobald Dateigrößen oder Nebenläufigkeit wachsen | Selten --- so machen es produktive Apps im großen Maßstab tatsächlich |
Wo das in einer von Greta gebauten App lebt
Weil Greta eine Next.js-App mit Prisma und Cloud-Storage von Anfang an einrichtet, ist das Signed-URL-Muster keine zusätzliche Infrastruktur, die man später anflanscht --- es ist die Standardform eines Upload-Features. Eine Route wie app/api/uploads/sign/route.ts übernimmt die Berechtigungsprüfung und fragt beim Storage die signierte URL an; eine documents- oder attachments-Tabelle im Prisma-Schema speichert den finalen Pfad, den Content-Type und den Eigentümer, sobald der Upload bestätigt ist. Die Bild-Transformationen und die CDN-Auslieferung, die Thumbnails schnell laden lassen, übernimmt die Storage-Ebene --- nichts, was du von Grund auf schreibst.
Das zählt bei allem mit echtem Dateivolumen --- ein CRM, bei dem jeder Kontakt Dokumente angehängt hat, ein Marktplatz, bei dem jedes Listing Fotos hat, ein internes Tool, bei dem jemand wöchentlich eine Tabelle hochlädt. Das Muster ändert sich nicht; nur die Dateitypen und Validierungsregeln.
FAQ
Kann ich Dateien einfach als Blob-Spalte in meiner Datenbank speichern? Kannst du, und für eine Handvoll kleiner Dateien wird nichts sichtbar kaputtgehen. Aber Datenbankspeicher ist pro Gigabyte teurer als Object Storage, Backups werden langsamer, je größer die Blob-Tabelle wird, und du verlierst die CDN-Auslieferung, die Bilder und Videos schnell laden lässt. Nutze die Datenbank für Metadaten und Pfad der Datei, nicht für die Bytes selbst.
Was ist mit Dateigrößenlimits für kostenlose vs. zahlende Nutzer? Das ist eine Geschäftsregel, keine technische --- prüfe sie im Schritt "signierte URL anfordern", bevor der Upload überhaupt startet, damit ein Nutzer über seinem Kontingent eine klare Meldung bekommt statt eines verschwendeten Uploads.
Brauche ich Virenscans? Für eine Consumer-App, bei der Nutzer ihre eigenen Dateien für den eigenen Gebrauch hochladen, in der Regel nicht am ersten Tag. Für alles, wo die Datei eines Nutzers später von einem anderen Nutzer geöffnet wird --- ein gemeinsamer Workspace, ein Support-Ticket-Anhang ---, ja, und dieser Scan sollte Teil des Bestätigungsschritts sein, bevor die Datei für alle anderen als verfügbar markiert wird.
Wie handhabe ich Bildgrößenänderungen für verschiedene Bildschirmgrößen? Die meisten Storage-Provider oder CDNs davor unterstützen On-the-fly-Transformationen über URL-Parameter --- dasselbe Bild in verschiedenen Breiten anfragen und das Resizing und Caching dem Edge überlassen, statt selbst fünf Versionen jedes Uploads zu generieren und zu speichern.
Abschluss
Datei-Uploads wirken wie ein gelöstes Problem --- bis jemand eine echte Datei sendet: ein Video, ein Stapel gescannter Rechnungen, eine Design-Datei, die eigentlich 80 MB groß ist. Baue das Signed-URL-Muster von Anfang an ein, und dieser Tag ist ein Nicht-Ereignis. Überspringe es, und es ist ein Produktionsvorfall mit deinem Namen im Post-Mortem.
