Neuigkeiten von trion.
Immer gut informiert.

File-Upload mit Nest.js

Nest.js Logo

Als Standard-Verfahren für Datei-Uploads kommt bei Nest.js-Anwendungen typischerweise ein Datei-Upload als multipart/form-data mit Hilfe der Multer-Bibliothek zum Einsatz. Dies ist jedoch nicht immer passend: Die Verwendung von multipart/form-data kommt typischerweise im Kontext von HTML Formularen vor. Ohne HTML Formular und eingebaute Logik eines Browser ist die Verwendung auf Clientseitig teilweise umständlich zu implementieren.

Im Kontext von Microservice Architekturen liegen manchmal Daten vor, die zwar als einzelne Verarbeitung behandelt werden sollen, die jedoch mit einem klassischen File-Upload Formular wenig zu tun haben.

Bei Nest.js kommt die aus dem Node / Express Umfeld bekannte Multer Bibliothek für Multipar-Uploads zum Einsatz. Mit einzelnen Binary-POSTs kann sie jedoch nicht umgehen.

In diesem Artikel werden wir einen Datei-Upload implementieren, der ohne Multer auskommt und stattdessen die Dateien direkt als Binärdaten einließt.

Nest.js Fileupload mit Multi-Part-Requests

Zum Vergleich der beiden Herangehensweisen gehen wir zunächst ein kurzes Beispiel für die herkömmliche Anbindung in Nest.js mit Multer durch.
Die von Nest.js direkt mitgelieferte Multer-Anbindung baut auf express auf und funktioniert so nur im Zusammenspiel mit der Nest.js-Express Anbindung, nicht jedoch mit dem alternativen Framework Fastify. Dies ist auch daran zu erkennen, dass der zugehörige mitgelieferte FileInterceptor aus dem Paket @nestjs/platform-express stammt.

In folgendem Code-Ausschnitt ist zu sehen, wie dem FileInterceptor der String foo übergeben wird. Dies bedeutet, dass sich die Datei, die hochgeladen werden soll, in über den Namen foo referenziert wird: Bei einem Multipart-Upload können ja gerade mehrere Dateien und Formularelemente gleichzeitig hochgeladen werden.

Mittels des @UploadedFile()-Decorators kann die so erhaltene Datei dann als Parameter in die Request-Handler-Methode hineingereicht werden.

Fileupload mit Nest.js via Multer
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller()
export class DemoController {

  @Post('form')
  @UseInterceptors(FileInterceptor('foo'))
  uploadFileForm(@UploadedFile() file) {
    console.log('form', file);
  }
}

Da Dateien von Multer im Default in Memory gehalten werden, sollte bei großen Dateien zur Begrenzung des Ressourcen-Bedarfs direkt ins Dateisystem geschrieben werden.
Aus Sicherheitsaspekten sollte weiterhin eine generelle Größenbeschränkung für hochgeladene Dateien vorgegeben werden, damit die Anwendung nicht durch fehlerhafte oder absichtlich zu große Requests zum Absturz gebracht werden kann.

Beides ist beim Import des MulterModule möglich und kann folgendermaßen aussehen:

Konfiguration des Datei-Uploads über das MulterModule
  MulterModule.register({
    dest: './dist/uploads',
    limits: {
      fileSize: 50 * 1024 * 1024, // f.e. 50MB
    },
  }),

Um zu testen, dass die Anbindung so funktioniert, kann ein Request, z.B. mit httpie, gegen den Endpunkt ausgeführt werden:

HTTP-Request zum Upload einer .zip-Datei.
$ http --form localhost:3000/form foo@~/demo/demo-file.zip

Multer kann den Datei-Typ dabei anhand der Endung der jeweiligen Datei ermitteln.

Nest.js Fileupload als Request Body

Es kann vorkommen, dass der obige Ansatz nicht passend ist, zum Beispiel, wenn der Client die gewünschten Daten direkt im Request-Body sendet. Möchte man derartige Requests verarbeiten, muss direkt auf den Input des Requests zugegriffen werden. Dieser Ansatz wird im folgenden gezeigt.

Dazu ist es wichtig zu wissen, dass Nest.js im Default-Fall express als HTTP-Adapter verwendet. Nest.js bietet auch die Möglichkeit, auf die zugrundeliegende Express-Infrastruktur zuzugreifen: Um sich die Express-Repräsentation des Request-Objektes zu besorgen, wird der @Req()-Decorator verwendet.

Der Request ist dabei ein Node (Readable-)Stream und damit kompatibel mit anderen Node Streams.
Es gibt im wesentlichen zwei APIs zum Umgang mit den Streams:

  • Per .pipe() mit einem Zielstream (z.B. Datei, Datenbankverbindung) verbinden, die Daten werden dann automatisch durchgereicht

  • Mittels Event-Handler auf Aktivitäten reagieren

Event-basierte Request-Behandlung

Zunächst werfen wir einen Blick auf die explizite Event-Behandlung. Der Express-Request bietet die Möglichkeit, direkte Event-Handler zu registrieren, mit dem einzelne Datenpakete empfangen und behandelt werden können.
Die folgenden Eventtypen gilt es zu berücksichtigen:

'data'

Ein Abschnitt der zu empfangenen Daten (bei chunked Encoding), oder alle Daten

'end'

Die Übertragung wurde beendet

'error'

Es ist ein Fehler aufgetreten

In folgendem Beispiel wird außerdem der HTTP Content-Length-Header abgefragt. Dieser kann genutzt werden, um z.B. eine Größenbeschränkung für hochgeladenen Dateien umzusetzen.

Event-basiertes Lesen des HTTP Request Streams in NestJS
import { Controller, Headers, Post, Req } from '@nestjs/common';
import { Request } from 'express';

@Controller()
export class DemoController {

  @Post('events')
  uploadFileStream(
    @Headers('content-length') length: number,
    @Req() req: Request
): void {
    if(length > 50 * 1024 * 1024){
      return;
    }

    let file = '';
    req.on('data', (d: any) => {
      file += d;
    });
    req.on('end', () => {
      console.log('end', file);
    });
  }
}

Auch hier kann die Funktionalität wieder mit Hilfe eines HTTPie-Requests verifiziert werden. Wichtig ist dabei, dass Nest.js bei einem Request mit Mimetype application/json dass der Request JSON-Daten enthält und auch gleich versucht, die Daten nach JSON zu parsen.
Um dies zu verhindern und zu erreichen, dass der Request als Stream behandelt wird, muss ein Content-Type-HTTP-Header gesetzt werden, der den Automatismus verhindert. Zum Beispiel application/octet-stream:

HTTP-Request zum Upload einer .zip-Datei per Event-Stream.
$ http localhost:3000/events Content-Type:application/octet-stream < ./demo/demo-file.zip

Request Daten mit .pipe()

Statt die Daten aus einzelnen Events zusammen zu setzen, können diese auch direkt als Stream behandelt werden. Dazu muss man sich abermals den nativen Request besorgen. Das Request-Objekt bietet dann die pipe()-Methode an, mit der der Request als Stream eingelesen und direkt an einen weiteren Stream weitergeleitet werden kann. In folgendem Beispiel wird der Stream zum Beispiel direkt in einen WriteStream umgeleitet, der die hochgeladene Datei ins Dateisystem schreibt. Generell kann der Datenstrom aus dem Request auf diese Weise an einen beliebigen Ziel-Stream weitergereicht werden.

Lesen des Streams in Form einer Pipe mit Ausgabe in Datei
import { Controller, Post, Req } from '@nestjs/common';
import { Request } from 'express';
import { createWriteStream, existsSync, mkdirSync } from 'fs';

@Controller()
export class DemoController {

  @Post('stream')
  uploadFileStream(@Req() req: Request): void {
    if (!existsSync('./dist/stream')) {
      mkdirSync('./dist/stream', { recursive: true });
    }
    const writeStream = createWriteStream(
      './dist/stream/upload' + Math.floor(Math.random() * 100),
    );
    req.pipe(writeStream);
  }
}

Der Request zum Upload der Datei sieht dabei genau so aus, wie im Falle der Event-Basierten Verarbeitung:

HTTP-Request zum Upload einer .zip-Datei per Stream.
$ http localhost:3000/stream Content-Type:application/octet-stream < ./demo/demo-file.zip




Zu den Themen Nest.js und Node bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:

Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.

Feedback oder Fragen zu einem Artikel - per Twitter @triondevelop oder E-Mail freuen wir uns auf eine Kontaktaufnahme!

Los geht's!

Bitte teilen Sie uns mit, wie wir Sie am besten erreichen können.