CSV-Dateien über eine REST API hochladen und verarbeiten
Senden Sie eine CSV-Datei als Multipart-Formularfeld namens file an
POST /upload und lesen Sie anschließend die importierten Datensätze aus
GET /data. Diese Anleitung erstellt diese Endpunkte mit Express und Multer und nutzt
csv-parse, um CSV in validierte JSON-Datensätze umzuwandeln. Ein erfolgreicher Import
ersetzt den bisherigen Datenbestand; ein abgelehnter Import lässt ihn unverändert.
Das Beispiel hält einen gemeinsamen Datenbestand im Arbeitsspeicher und lauscht nur auf Ihrem lokalen Rechner. Es dient als kleine Importdemonstration: Ein Neustart des Servers löscht die Daten, und es gibt weder eine Anmeldung noch einen Speicher pro Benutzer.
Voraussetzungen
Verwenden Sie Node.js 24.x oder 26.x, Corepack mit verfügbarem Yarn 4.12.0 und ein Bash-kompatibles
Terminal. Die Befehle benötigen außerdem cURL 7.76.0 oder neuer für
--fail-with-body. Das Beispiel wurde unter macOS mit
Node.js 24.21.0 und 26.8.2, Yarn 4.12.0 sowie cURL 8.7.1 getestet.
Unser CSV-Format enthält die Kopfzeile name,age in dieser Reihenfolge und mindestens
eine Datenzeile. Namen dürfen weder leer sein noch ausschließlich aus Leerraum bestehen; Altersangaben
müssen aus einer bis drei Dezimalziffern bestehen und zwischen null und 130 liegen. Der Importer
entfernt umgebenden Leerraum aus Werten und wandelt Altersangaben in JSON-Zahlen um. Verwenden Sie
kommagetrennte Eingaben in UTF-8; eine UTF-8-Byte-Order-Mark sowie Kommas und Zeilenumbrüche in
Anführungszeichen werden unterstützt. Leere Zeilen werden übersprungen.
Einen Server mit Node.js und Express einrichten
Führen Sie dies in dem Verzeichnis aus, in dem der neue Ordner csv-upload-api entstehen
soll. Die Subshell lässt Ihr aktuelles Verzeichnis unverändert. Falls dieser Ordner bereits existiert,
stoppt der Befehl, ohne Pakete zu installieren oder seine Dateien zu überschreiben. Wählen Sie einen
neuen Speicherort, um die Einrichtung zu wiederholen.
(
mkdir csv-upload-api &&
cd csv-upload-api &&
printf '{"name":"csv-upload-api","private":true,"packageManager":"yarn@4.12.0"}\n' > package.json &&
touch yarn.lock &&
corepack yarn config set nodeLinker node-modules &&
corepack yarn add --exact express@5.2.1 multer@2.4.0 csv-parse@7.0.2
)
Die leere Lockdatei macht daraus ein eigenständiges Yarn-Projekt, auch wenn Sie es innerhalb eines
anderen Projekts erstellen. Bewahren Sie die generierte Datei yarn.lock zusammen
mit Ihrem Beispiel auf, um die aufgelösten Abhängigkeitsversionen beizubehalten. Multer 2.4.0 enthält die
Korrektur für eine Schwachstelle beim Löschen temporärer Dateien nach abgebrochenen Uploads;
halten Sie diese Abhängigkeit mit Patches aktuell, wenn Sie das Beispiel anpassen.
Den Endpunkt für Datei-Uploads implementieren
Erstellen Sie server.js im neuen Verzeichnis csv-upload-api mit dem
vollständigen Code unten. Multer akzeptiert eine Datei mit bis zu 5 MiB und keine zusätzlichen
Formularfelder. Seine
Middleware single('file')
schreibt eine temporäre Datei und stellt sie als req.file bereit.
const fs = require('node:fs')
const path = require('node:path')
const { pipeline } = require('node:stream/promises')
const { CsvError, parse } = require('csv-parse')
const express = require('express')
const multer = require('multer')
const app = express()
const port = Number(process.env.PORT || 3100)
const upload = multer({
dest: path.join(__dirname, 'uploads'),
limits: { fileSize: 5 * 1024 * 1024, files: 1, fields: 0, parts: 1 },
})
class InvalidCsv extends Error {}
let storedData = []
async function parseUpload(filePath) {
try {
const results = []
await pipeline(
fs.createReadStream(filePath),
parse({
bom: true,
skip_empty_lines: true,
max_record_size: 64 * 1024,
columns(header) {
if (header.length !== 2 || header[0] !== 'name' || header[1] !== 'age') {
throw new InvalidCsv('Expected the header name,age.')
}
return header
},
}),
async (rows) => {
for await (const row of rows) {
const name = row.name.trim()
const age = row.age.trim()
if (name === '' || !/^\d{1,3}$/.test(age) || Number(age) > 130) {
throw new InvalidCsv('Each row needs a name and an integer age from 0 to 130.')
}
if (results.length === 10000) {
throw new InvalidCsv('At most 10,000 records are allowed.')
}
results.push({ name, age: Number(age) })
}
},
)
if (results.length === 0) {
throw new InvalidCsv('Include at least one data row.')
}
return results
} finally {
await fs.promises.unlink(filePath)
}
}
app.post('/upload', upload.single('file'), async (req, res) => {
if (!req.file) {
return res.status(400).json({ error: 'Send a CSV file in the file field.' })
}
const results = await parseUpload(req.file.path)
storedData = results
res.json({ recordsStored: results.length })
})
app.get('/data', (req, res) => {
res.json(storedData)
})
// Express recognizes error middleware by its four arguments; register it after the routes.
app.use((error, req, res, next) => {
if (res.headersSent) return next(error)
if (error instanceof multer.MulterError) {
const tooLarge = error.code === 'LIMIT_FILE_SIZE'
return res.status(tooLarge ? 413 : 400).json({
error: tooLarge ? 'The file exceeds 5 MiB.' : 'Send one file field and no other fields.',
})
}
if (error instanceof InvalidCsv) {
return res.status(400).json({ error: error.message })
}
if (error instanceof CsvError) {
return res.status(400).json({ error: 'Invalid CSV syntax or record too large.' })
}
console.error('CSV import failed because of an unexpected server error.')
res.status(500).json({ error: 'Unable to import the file.' })
})
app.listen(port, '127.0.0.1', () => {
console.log(`CSV API listening on http://127.0.0.1:${port}`)
})
Der Endpunkt prüft den Inhalt unabhängig vom Dateinamen oder MIME-Typ, den der Client angibt. Diese
Angaben belegen nicht, dass eine Datei gültiges CSV enthält. Auch eine Datei namens
data.csv muss die Parser- und Zeilenprüfungen bestehen.
CSV-Daten parsen und speichern
Der Callback columns des Parsers prüft die
Kopfzeile, bevor Datensatzobjekte erstellt werden. Die Vorgabe, dass sie exakt
name,age entsprechen muss, weist doppelte, fehlende und unerwartete Spalten zurück.
Der Parser lehnt uneinheitliche Zeilenlängen und
Felder mit nicht geschlossenen Anführungszeichen ab. Die Schleife
validiert die Werte, bevor sie sie dem neuen Datenbestand hinzufügt.
pipeline wartet, bis Lesen, Parsen und Zeilenvalidierung abgeschlossen sind. Der
Block finally entfernt anschließend die temporäre Datei, auch wenn das Parsen
fehlschlägt. Erst danach ersetzt /upload den Wert von
storedData. Express 5 leitet abgelehnte Promises asynchroner Routen weiter
an die Fehler-Middleware. Ein Dateisystemfehler liefert daher eine allgemeine 500-Antwort statt einer
Parser-Meldung oder eines lokalen Pfads.
Neben der Dateigrößengrenze von 5 MiB begrenzt
max_record_size
die Datensatzpuffer des Parsers, und die Schleife beschränkt die Ausgabe auf 10.000 Datensätze. Das
Parsen nutzt Streams, doch die fertigen Datensätze belegen weiterhin Arbeitsspeicher. Beim Ersetzen
können sowohl der bisherige Datenbestand als auch die neuen Datensätze gleichzeitig vorhanden sein.
Parallele Uploads sind unabhängig voneinander; der letzte erfolgreich abgeschlossene Upload setzt
sich durch.
Die API testen
Starten Sie den Server in einem Terminal aus dem Verzeichnis, das csv-upload-api enthält:
(cd csv-upload-api && node server.js)
Lassen Sie ihn laufen und verwenden Sie ein zweites Terminal im selben übergeordneten Verzeichnis.
Dieser Befehl erstellt data.csv innerhalb des Projekts.
noclobber verhindert, dass er eine vorhandene Datei überschreibt, auch bei einer
nicht interaktiven erneuten Ausführung.
(
cd csv-upload-api &&
set -o noclobber &&
cat > data.csv <<'CSV'
name,age
"Doe, Jane",34
Tim,42
CSV
)
Laden Sie diese Datei hoch. -F erstellt eine Multipart-Anfrage;
file muss dem Feldnamen des Servers entsprechen. Lassen Sie cURL die
Multipart-Grenze angeben, statt den Header Content-Type der Anfrage selbst zu setzen.
curl --fail-with-body --silent --show-error \
-F 'file=@csv-upload-api/data.csv;type=text/csv' http://127.0.0.1:3100/upload
Die Antwort lautet {"recordsStored":2}. Rufen Sie die gespeicherten Datensätze ab:
curl --fail-with-body --silent --show-error http://127.0.0.1:3100/data
[{"name":"Doe, Jane","age":34},{"name":"Tim","age":42}]
Senden Sie nun eine Zeile ohne Altersangabe. Diese Anfrage liest CSV aus der Standardeingabe und erstellt oder ersetzt daher keine lokale Datei:
curl --fail-with-body --silent --show-error --write-out '\nHTTP %{http_code}\n' \
-F 'file=@-;filename=invalid.csv;type=text/csv' http://127.0.0.1:3100/upload <<'CSV'
name,age
Tim
CSV
Erwarten Sie HTTP 400 und {"error":"Invalid CSV syntax or record too large."}. cURL beendet sich wegen
--fail-with-body mit Code 22; diese Ablehnung ist das beabsichtigte Ergebnis. Führen Sie
den Befehl GET /data erneut aus: Jane und Tim sollten weiterhin vorhanden sein.
Wenn Sie den erfolgreichen Upload wiederholen, wird der Datenbestand durch dieselben zwei Datensätze
ersetzt, ohne Duplikate anzuhängen.
Weitere sinnvolle Prüfungen sind eine leere Datei oder eine CSV-Datei mit ausschließlich einer
Kopfzeile, die jeweils 400 zurückgeben, sowie eine Datei mit mehr als 5 MiB, die 413 zurückgibt. Ein
fehlender Teil namens file, ein anderer Feldname, mehrere Dateien oder
zusätzliche Textfelder führen ebenfalls zu 400. Fehler durch ungültiges CSV sind Clientfehler; ein
unerwarteter Fehler beim Speichern liefert 500.
Wählen Sie in Postman POST und http://127.0.0.1:3100/upload.
Fügen Sie unter Body → form-data einen einzelnen Schlüssel namens
file hinzu, setzen Sie seinen Typ auf File und wählen Sie
data.csv aus. Lassen Sie Postman den Header Content-Type der
Anfrage einschließlich der Multipart-Grenze generieren. Stoppen Sie den Server zum Abschluss mit
Ctrl+C.
Hinweise zum Produktivbetrieb
Bevor Sie diesen Endpunkt zugänglich machen, ergänzen Sie eine Authentifizierung und prüfen Sie die Berechtigungen sowohl für den Import als auch für das Lesen jedes Datenbestands. Ersetzen Sie das gemeinsame Array durch dauerhaften Speicher und verwenden Sie eine Transaktion oder einen gestuften Import, damit eine fehlerhafte Zeile keinen teilweise aktualisierten Datenbestand hinterlassen kann. Authentifizierung allein isoliert die Datensätze eines Benutzers nicht von denen eines anderen.
Speichern Sie temporäre Uploads außerhalb öffentlich ausgelieferter Verzeichnisse. Die hier gezeigte Bereinigung behandelt übliche Anfrage- und Parserfehler; ein Prozessabsturz kann Dateien zurücklassen. Ein bereitgestellter Dienst benötigt daher auch eine Regelung zum Entfernen verwaister Uploads. Setzen Sie Zeitlimits für Anfragen sowie Grenzen für die Anfragerate und die Anzahl gleichzeitiger Anfragen: Eine Grenze pro Datei begrenzt nicht den gesamten Arbeitsspeicher- und Festplattenbedarf vieler gleichzeitiger Anfragen. Größere Importe benötigen in der Regel einen Hintergrundjob und einen Endpunkt für den Importstatus.
