REST API design - 15 - Upload de fichiers : multipart, signed URLs et streaming

multipart/form-data, limites de taille, presigned URLs S3, streaming et traitement d'images dans une API REST.

REST API design 16 / 25
  1. 01 REST API design - 00 - Pourquoi le design de ton API change tout
  2. 02 REST API design - 01 - Les principes REST que personne ne lit
  3. 03 REST API design - 02 - Des URLs qui ont du sens
  4. 04 REST API design - 03 - Les méthodes HTTP, pour de vrai
  5. 05 REST API design - 04 - Les codes de statut HTTP qu'il faut connaître
  6. 06 REST API design - 05 - Body, headers et le diable dans les détails
  7. 07 REST API design - 06 - La pagination, ou comment ne pas tuer ta base
  8. 08 REST API design - 07 - Filtrage et tri sans prise de tête
  9. 09 REST API design - 08 - La validation avec Zod, gardien de ton API
  10. 10 REST API design - 09 - Erreurs : un format que tes clients vont adorer
  11. 11 REST API design - 10 - Authentification : JWT, API keys et OAuth2
  12. 12 REST API design - 11 - Versioning : quand et comment faire évoluer ton API
  13. 13 REST API design - 12 - CORS : comprendre et debugger les erreurs cross-origin
  14. 14 REST API design - 13 - Rate limiting : protéger ton API sans frustrer tes clients
  15. 15 REST API design - 14 - Caching : les bonnes réponses sont celles qu'on n'envoie pas
  16. 16 REST API design - 15 - Upload de fichiers : multipart, signed URLs et streaming
  17. 17 REST API design - 16 - Relations entre ressources : embarquer, lier ou les deux
  18. 18 REST API design - 17 - HATEOAS : des liens dans tes réponses
  19. 19 REST API design - 18 - OpenAPI : le contrat entre ton API et le monde
  20. 20 REST API design - 19 - Documentation : une API non documentee est une API inutile
  21. 21 REST API design - 20 - Testing : tester ton API sans devenir fou
  22. 22 REST API design - 21 - Webhooks : quand c'est ton API qui appelle
  23. 23 REST API design - 22 - Performance : quand chaque milliseconde compte
  24. 24 REST API design - 23 - Sécurité : les attaques que tu vas subir (et comment t'en protéger)
  25. 25 REST API design - 24 - Glossaire : tous les termes REST API en un seul endroit

15 - Upload de fichiers : multipart, signed URLs et streaming

Ce que tu vas apprendre

  • Comment fonctionne multipart/form-data
  • Les limites de taille et la validation de fichiers
  • L'approche presigned URL avec S3
  • Le streaming pour les gros fichiers
  • Le traitement d'images cote serveur

Prerequisites

Avoir lu l'article sur le caching. Savoir ce qu'est un bucket S3 (ou équivalent) est utile mais pas indispensable.


Mon premier upload de fichier en API, c'etait un endpoint qui acceptait du base64 dans un champ JSON. L'image de 2 Mo devenait 2.7 Mo en base64. Le body JSON complet faisait 3 Mo. Le parser JSON chargeait tout en mémoire d'un coup. Avec 50 uploads simultanes, le serveur manquait de RAM. J'ai appris a la dure que JSON et fichiers binaires ne font pas bon menage.

multipart/form-data

Le format standard pour envoyer des fichiers en HTTP. Le body est decoupe en "parts" separees par un boundary :

POST /products/42/images HTTP/1.1
Content-Type: multipart/form-data; boundary=----formdata123

------formdata123
Content-Disposition: form-data; name="title"

Photo principale
------formdata123
Content-Disposition: form-data; name="file"; filename="burger.jpg"
Content-Type: image/jpeg

[donnees binaires]
------formdata123--

Chaque part a son propre Content-Type. Tu peux mixer du texte et des fichiers dans la meme requête. En Express avec multer :

typescriptimport multer from "multer";

const upload = multer({
  limits: { fileSize: 5 * 1024 * 1024 }, // 5 Mo max
  fileFilter: (req, file, cb) => {
    const allowed = ["image/jpeg", "image/png", "image/webp"];
    if (allowed.includes(file.mimetype)) {
      cb(null, true);
    } else {
      cb(new Error("Type de fichier non autorise"));
    }
  }
});

app.post("/products/:id/images", upload.single("file"), (req, res) => {
  // req.file contient le fichier uploade
  res.status(201).json({ url: `/images/${req.file.filename}` });
});

La validation, c'est pas optionnel

Ne fais jamais confiance au Content-Type envoye par le client. Un fichier .exe renomme en .jpg aura toujours image/jpeg comme mimetype. Verifie les magic bytes (les premiers octets du fichier) :

typescriptimport { fileTypeFromBuffer } from "file-type";

const buffer = req.file.buffer;
const type = await fileTypeFromBuffer(buffer);

if (!type || !["image/jpeg", "image/png"].includes(type.mime)) {
  return res.status(422).json({
    type: "https://api.example.com/errors/invalid-file-type",
    title: "Invalid File Type",
    status: 422,
    detail: "Seuls les fichiers JPEG et PNG sont acceptes."
  });
}

Limite aussi la taille. Pas seulement dans multer, mais aussi au niveau du reverse proxy (nginx client_max_body_size) pour rejeter les requêtes trop grosses avant qu'elles n'atteignent ton app.

Presigned URLs : le client upload directement

Pour les gros fichiers, faire transiter les donnees par ton serveur est du gaspillage. Avec les presigned URLs, ton serveur généré une URL temporaire et le client upload directement vers S3 :

1. Client -> API : POST /uploads (demande une URL d'upload)
2. API -> Client : { url: "https://s3.../presigned", fields: {...} }
3. Client -> S3 : PUT avec le fichier (ton serveur n'est pas implique)
4. Client -> API : POST /products/42/images { key: "uploads/abc.jpg" }
typescriptimport { S3Client, PutObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";

const s3 = new S3Client({ region: "eu-west-1" });

app.post("/uploads", async (req, res) => {
  const key = `uploads/${crypto.randomUUID()}.jpg`;
  const command = new PutObjectCommand({
    Bucket: "mon-bucket",
    Key: key,
    ContentType: "image/jpeg"
  });

  const url = await getSignedUrl(s3, command, { expiresIn: 300 });
  res.json({ url, key });
});

L'URL expire apres 5 minutes. Le client ne peut uploader que le type de fichier prevu. Ton serveur n'a jamais touche au fichier. J'utilise cette approche sur paltemps.fr pour tous les uploads d'images.

Streaming pour les gros fichiers

Si tu dois quand meme faire transiter le fichier par ton serveur (transformation, virus scan), utilise le streaming pour ne pas charger tout en mémoire :

typescriptimport { pipeline } from "stream/promises";
import { createWriteStream } from "fs";

app.post("/uploads/stream", async (req, res) => {
  const dest = createWriteStream(`/tmp/${crypto.randomUUID()}`);
  await pipeline(req, dest);
  res.status(201).json({ message: "Upload termine" });
});

Avec busboy pour parser le multipart en streaming :

typescriptimport busboy from "busboy";

app.post("/uploads", (req, res) => {
  const bb = busboy({ headers: req.headers, limits: { fileSize: 50 * 1024 * 1024 } });

  bb.on("file", (name, stream, info) => {
    const dest = createWriteStream(`/tmp/${info.filename}`);
    stream.pipe(dest);
  });

  bb.on("finish", () => res.status(201).json({ ok: true }));
  req.pipe(bb);
});

Traitement d'images

Redimensionner, convertir en WebP, générer des thumbnails. Sharp est la référencé en Node.js :

typescriptimport sharp from "sharp";

async function processImage(input: Buffer) {
  const original = await sharp(input)
    .resize(1200, 1200, { fit: "inside", withoutEnlargement: true })
    .webp({ quality: 80 })
    .toBuffer();

  const thumbnail = await sharp(input)
    .resize(300, 300, { fit: "cover" })
    .webp({ quality: 70 })
    .toBuffer();

  return { original, thumbnail };
}

Pour les gros volumes, deporte le traitement dans une queue (BullMQ, SQS) plutot que de bloquer la requête HTTP. Le client recoit un 202 Accepted et peut poller un endpoint de statut.

Résumé

  • Utilise multipart/form-data pour les uploads, jamais du base64 dans du JSON
  • Valide le type réel du fichier avec les magic bytes, pas le mimetype déclaré
  • Les presigned URLs permettent au client d'uploader directement vers S3
  • Utilise le streaming (busboy) pour ne pas charger les gros fichiers en mémoire
  • Deporte le traitement d'images dans une queue pour ne pas bloquer les requêtes

Article précédent : Caching Article suivant : Relations entre ressources

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.