Servir des fichiers statiques dans NestJS

Servir des fichiers statiques dans NestJS

Lorsqu'une application NestJS gère des uploads (images, documents, avatars), il devient rapidement nécessaire de servir ces fichiers via une URL publique. Plutôt que de coder manuellement un contrôleur qui lit le système de fichiers, NestJS propose un module dédié : @nestjs/serve-static. Dans ce tutoriel, nous allons voir comment l'intégrer proprement, en se basant sur la configuration réelle du projet site-voyance (Le Murmure des Cartes).

Prérequis

  • Node.js 18+ et un projet NestJS 10+ déjà initialisé
  • Un dossier uploads/ à la racine du projet (où seront stockés les fichiers)
  • Notions de base sur les modules NestJS

1. Installation du module

La première étape consiste à installer le package officiel @nestjs/serve-static. Il s'appuie sur Express (ou Fastify) pour exposer un dossier du système de fichiers comme une route HTTP statique.

npm install @nestjs/serve-static

Aucune dépendance supplémentaire n'est requise si vous utilisez l'adaptateur Express par défaut, qui est embarqué dans NestJS.

2. Configuration dans AppModule

Le module s'enregistre via la méthode statique ServeStaticModule.forRoot(). Voici l'extrait directement issu du fichier app.module.ts du projet :

import { Module } from '@nestjs/common';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { AppController } from './app.controller';
import { AppService } from './app.service';

@Module({
  imports: [
    // Serve uploaded files
    ServeStaticModule.forRoot({
      rootPath: join(__dirname, '..', 'uploads'),
      serveRoot: '/uploads',
    }),
    // ... autres modules
  ],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Deux options principales sont utilisées ici :

  • rootPath : le chemin absolu vers le dossier physique à exposer. On utilise join(__dirname, '..', 'uploads') pour remonter d'un niveau depuis le dossier dist/ compilé jusqu'à la racine du projet.
  • serveRoot : le préfixe d'URL public sous lequel les fichiers seront accessibles. Ici, un fichier uploads/avatar.jpg sera disponible à l'URL http://localhost:3002/uploads/avatar.jpg.

3. Pourquoi utiliser path.join ?

Construire un chemin avec une simple concaténation de chaînes (__dirname + '/../uploads') fonctionne sur Linux et macOS, mais pose problème sur Windows où le séparateur est \. La fonction join() de Node.js gère automatiquement le bon séparateur selon le système d'exploitation, ce qui rend votre code portable.

Architecture d'un serveur NestJS servant des fichiers statiques
Image générée par Google Gemini

4. Options avancées de ServeStaticModule

Au-delà de rootPath et serveRoot, le module accepte plusieurs options utiles :

ServeStaticModule.forRoot({
  rootPath: join(__dirname, '..', 'uploads'),
  serveRoot: '/uploads',
  exclude: ['/api/(.*)'],
  serveStaticOptions: {
    index: false,
    maxAge: 86400000, // 1 jour de cache navigateur
    immutable: true,
    etag: true,
    setHeaders: (res, path) => {
      if (path.endsWith('.jpg') || path.endsWith('.png') || path.endsWith('.webp')) {
        res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
      }
    },
  },
}),

Détaillons :

  • exclude : exclut certaines routes du middleware statique. Indispensable si votre API expose un préfixe global comme /api (c'est le cas dans main.ts avec app.setGlobalPrefix('api')) afin que les requêtes API ne soient pas interceptées.
  • serveStaticOptions.index : désactive le service automatique de index.html dans les sous-dossiers.
  • maxAge et setHeaders : permettent de contrôler les en-têtes de cache HTTP, essentiels pour les performances.
  • etag : active la génération d'ETag pour des revalidations efficaces côté navigateur.

5. CORS et accès aux images depuis le frontend

Lorsque le frontend (par exemple un Next.js sur localhost:3000) charge des images servies par l'API sur localhost:3002, des restrictions CORS peuvent s'appliquer, notamment pour des opérations comme <canvas> ou fetch. Le projet configure déjà CORS dans main.ts :

app.enableCors({
  origin: origins,
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
  allowedHeaders: ['Content-Type', 'Authorization', 'stripe-signature'],
  credentials: true,
});

Notez aussi cette ligne dans la configuration helmet :

app.use(
  helmet({
    contentSecurityPolicy: false,
    crossOriginEmbedderPolicy: false, // Nécessaire pour les images uploadées
  }),
);

Le fait de désactiver crossOriginEmbedderPolicy est explicitement justifié par le besoin d'embarquer des images uploadées sur des pages tierces (frontend séparé). Sans cela, helmet ajoute un en-tête Cross-Origin-Embedder-Policy: require-corp qui bloquerait le chargement des images.

6. Sécuriser certains fichiers

Par défaut, ServeStaticModule rend tous les fichiers du dossier publiquement accessibles à condition de connaître leur URL. C'est acceptable pour des images publiques (avatars, illustrations), mais pas pour des documents sensibles (factures, justificatifs).

Pour des fichiers privés, deux stratégies coexistent :

Option A : séparer les dossiers

Conservez un dossier uploads/public/ servi par ServeStaticModule, et un dossier uploads/private/ jamais exposé. Pour servir ces derniers, créez un contrôleur protégé par un guard JWT :

import { Controller, Get, Param, Res, UseGuards } from '@nestjs/common';
import { Response } from 'express';
import { join } from 'path';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';

@Controller('files')
@UseGuards(JwtAuthGuard)
export class PrivateFilesController {
  @Get(':filename')
  serveFile(@Param('filename') filename: string, @Res() res: Response) {
    const filePath = join(__dirname, '..', '..', 'uploads', 'private', filename);
    return res.sendFile(filePath);
  }
}

Option B : URLs signées

Générez des URLs temporaires avec un token signé, validez-les dans un middleware, puis laissez le module statique servir le fichier. Cette approche est plus complexe mais évite de surcharger l'API.

7. Optimisation : compression

Pour les fichiers texte (SVG, JSON, CSS), activer la compression gzip réduit drastiquement la bande passante. Installez et configurez le middleware compression :

npm install compression
npm install --save-dev @types/compression
import * as compression from 'compression';

// Dans bootstrap(), avant app.listen()
app.use(compression());

Pour les images JPEG/PNG/WebP, la compression à la volée est inutile (les formats sont déjà compressés). En revanche, pré-générer plusieurs résolutions au moment de l'upload reste une excellente pratique.

8. En production : déléguer à Nginx ou Traefik

Faire transiter chaque requête de fichier statique par Node.js fonctionne, mais reste sous-optimal. Un reverse proxy comme Nginx peut servir les fichiers directement depuis le disque, beaucoup plus rapidement, et libérer Node.js pour les requêtes applicatives.

Exemple de configuration Nginx :

server {
  listen 80;
  server_name api.murmure-des-cartes.cloud;

  # Fichiers statiques servis directement
  location /uploads/ {
    alias /var/www/site-voyance/uploads/;
    expires 30d;
    add_header Cache-Control "public, immutable";
    access_log off;
  }

  # Reste de l'API vers NestJS
  location / {
    proxy_pass http://localhost:3002;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
  }
}

Vous pouvez alors conserver ServeStaticModule en développement pour la simplicité, et laisser Nginx prendre le relais en production. Le code applicatif n'a pas besoin de changer.

Conclusion

Servir des fichiers statiques avec @nestjs/serve-static est extrêmement simple : une dépendance, trois lignes de configuration et vos fichiers sont accessibles. Mais derrière cette simplicité, il faut penser aux chemins cross-platform (avec path.join), à la sécurité (séparer public et privé, configurer helmet), au cache HTTP et à la répartition des responsabilités entre Node.js et un reverse proxy en production.

Pour aller plus loin, explorez la documentation officielle NestJS, ainsi que la mise en place d'un module d'upload avec @nestjs/platform-express et multer pour gérer le pendant écriture de cette architecture.

Commentaires

Soyez le premier à laisser un commentaire !

Laisser un commentaire

Les champs obligatoires sont indiqués avec *