Self-hosting

Mettre une API Node.js derrière un reverse proxy Nginx

12 min de lecture

📍 Article principal du parcours : Nginx : reverse proxy, HTTPS et configuration de A à Z
Cet article fait partie du parcours Nginx. Pour la vue d’ensemble, lisez d’abord le guide principal.

L’API de Colis Express tourne sur le port 3000 — mais vous n’allez pas demander à vos utilisateurs de taper :3000 dans leur navigateur, ni exposer Node directement à Internet. Nginx s’assoit devant : il écoute sur le port public, reçoit les requêtes, et relaie celles qui concernent l’API vers le service local. Le visiteur ne voit qu’une adresse propre ; le port 3000, lui, reste invisible et fermé au monde extérieur. À la fin de ce tutoriel, votre front statique et votre API cohabiteront sous un même domaine, le tout passant par Nginx.

🎯 Ce que vous allez apprendre

  • Faire tourner une API sur la boucle locale et comprendre pourquoi on ne l’expose jamais directement.
  • Écrire un bloc location avec proxy_pass pour relayer vers une application interne.
  • Transmettre les bons en-têtes pour que l’application connaisse le vrai client.
  • Servir le front statique et l’API derrière un seul et même domaine.
  • Diagnostiquer les erreurs 502 et 504, les plus courantes du reverse proxy.

🛠️ Ce que vous allez construire

Vous aurez un domaine unique où / sert le front statique vu au tutoriel précédent, et /api/ relaie vers l’API Node.js qui répond en local. Le port 3000 ne sera accessible que depuis le serveur lui-même ; tout passe par Nginx, ce qui prépare le terrain pour le HTTPS, le cache et la répartition de charge à venir.

Prérequis

  • Nginx installé et un site statique déjà servi (voir Installer Nginx et configurer ses premiers vhosts).
  • Node.js installé sur le serveur (version 20 LTS ou plus récente).
  • Niveau intermédiaire. Test express : si vous savez lancer un script Node et écrire un bloc server, vous êtes prêt.
  • ⏱️ Temps estimé : ~35 minutes.

Étape 1 — Faire tourner une API locale

Pour avoir quelque chose de concret à relayer, démarrons une mini-API. Le code n’est pas le sujet ici ; ce qui compte, c’est qu’elle écoute uniquement sur 127.0.0.1, l’interface locale, et non sur 0.0.0.0 qui l’exposerait sur toutes les interfaces réseau.

// api.js — l'API de Colis Express, réduite à l'essentiel
const http = require("http");
const server = http.createServer((req, res) => {
  res.writeHead(200, { "Content-Type": "application/json" });
  res.end(JSON.stringify({ service: "colis-express", statut: "ok" }));
});
// On écoute SEULEMENT sur la boucle locale
server.listen(3000, "127.0.0.1", () => {
  console.log("API à l'écoute sur 127.0.0.1:3000");
});

Lancez-la avec node api.js. Le détail crucial est le second argument de listen : en liant le service à 127.0.0.1, on garantit qu’il n’est joignable que depuis la machine elle-même. Même si le port 3000 était ouvert par erreur sur le pare-feu, personne de l’extérieur ne pourrait l’atteindre. C’est Nginx, et lui seul, qui fera le pont entre l’extérieur et ce service. En production, on confierait le maintien en vie de ce processus à un gestionnaire comme systemd ou PM2, mais pour ce tutoriel un simple node api.js suffit.

Point d’étape — Depuis le serveur, curl http://127.0.0.1:3000 renvoie le JSON {"service":"colis-express","statut":"ok"}. Depuis l’extérieur, ce port ne doit pas répondre.

Étape 2 — Le bloc location avec proxy_pass

Voici le cœur du reverse proxy. On reprend le bloc server du site et on y ajoute un location /api/ qui transmet à l’application. Ouvrez la configuration du site :

sudo nano /etc/nginx/sites-available/colis-express

Et complétez le bloc ainsi :

server {
    listen 80;
    server_name colis-express.net;

    root /var/www/colis-express/html;
    index index.html;

    # Le front statique
    location / {
        try_files $uri $uri/ =404;
    }

    # L'API : tout /api/ est relayé vers Node
    location /api/ {
        proxy_pass http://127.0.0.1:3000/;
    }
}

La directive proxy_pass fait tout le travail de relais. Un point mérite une attention particulière : la barre oblique finale dans proxy_pass http://127.0.0.1:3000/;. Avec cette barre, Nginx remplace le préfixe /api/ par / avant de transmettre : une requête vers /api/colis arrive à l’application comme /colis. Sans la barre finale, le chemin complet /api/colis serait transmis tel quel. Ce détail est la source d’innombrables heures de débogage ; retenez-le.

Étape 3 — Transmettre les bons en-têtes

Tel quel, le proxy fonctionne, mais l’application est aveugle : elle croit que toutes les requêtes viennent de 127.0.0.1 et ignore le vrai nom de domaine comme le protocole d’origine. Pour lui rendre la vue, on ajoute quelques en-têtes. C’est l’étape que les tutoriels pressés oublient, et celle qui cause le plus de bugs subtils.

    location /api/ {
        proxy_pass http://127.0.0.1:3000/;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

Chacun a un rôle précis. Host $host transmet le nom de domaine demandé, indispensable si l’application sert plusieurs domaines ou génère des liens absolus. X-Real-IP donne l’adresse IP du client direct. X-Forwarded-For empile les IP traversées, ce qui permet à l’application de retrouver le client d’origine même derrière plusieurs proxies — la variable $proxy_add_x_forwarded_for gère cet empilement automatiquement. Enfin, X-Forwarded-Proto $scheme indique si la connexion d’origine était en http ou https : sans lui, une application derrière un proxy HTTPS croit recevoir du HTTP et peut générer des redirections en boucle. Validez et rechargez avec nginx -t && sudo systemctl reload nginx.

Point d’étapecurl http://colis-express.net/api/ renvoie le JSON de l’API, et côté Node, les logs montrent désormais le vrai nom d’hôte et le bon protocole.

Étape 4 — Gérer les connexions WebSocket et le délai

Si votre API utilise des WebSockets (suivi de livraison en temps réel, par exemple), il faut autoriser explicitement la « montée en version » du protocole, que Nginx ne transmet pas par défaut. On en profite pour fixer un délai d’attente raisonnable vers le backend.

    location /api/ {
        proxy_pass http://127.0.0.1:3000/;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Support WebSocket
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection "upgrade";

        # Délai vers le backend
        proxy_read_timeout 60s;
    }

Le couple proxy_http_version 1.1; et les en-têtes Upgrade/Connection sont ce qui permet à une connexion HTTP classique de basculer en WebSocket à travers le proxy. Si votre API n’utilise pas de WebSocket, ces trois lignes ne gênent en rien et vous éviteront un casse-tête le jour où vous en ajouterez. Le proxy_read_timeout définit combien de temps Nginx attend une réponse du backend avant d’abandonner ; 60 secondes est confortable pour une API, à ajuster selon vos traitements longs.

Étape 5 — Vérification finale

Prouvons que l’ensemble tient debout de bout en bout : le front statique répond à la racine, l’API répond sous /api/, et le port 3000 reste injoignable de l’extérieur.

# Le front statique
curl -I http://colis-express.net/
# L'API via Nginx
curl http://colis-express.net/api/
# Le port direct depuis l'extérieur : doit échouer ou être filtré
curl --max-time 5 http://VOTRE_IP_PUBLIQUE:3000/

Les deux premières commandes doivent réussir : un 200 OK pour le front, le JSON pour l’API. La troisième, lancée depuis une autre machine, doit échouer (connexion refusée ou délai dépassé) — c’est la preuve que l’API n’est pas exposée et que tout passe bien par Nginx. Vous avez maintenant une architecture propre : un seul point d’entrée public, des services internes protégés.

Point d’étape — Front et API répondent via le domaine, le port 3000 est inaccessible depuis l’extérieur. L’architecture reverse proxy est en place.

Étape 6 — Servir un front moderne et régler le tampon

Aujourd’hui, le front de Colis Express ne restera pas une simple page HTML : ce sera probablement une application monopage (React, Vue, Svelte) dont le routage se fait côté navigateur. Or, avec une telle application, rafraîchir la page sur une URL comme /suivi/12345 renvoie une 404 : ce chemin n’existe pas sur le disque, c’est le JavaScript qui le gère une fois la page chargée. La parade côté Nginx est un fallback : toute URL qui ne correspond à aucun fichier réel est renvoyée vers index.html, qui rendra la main au routeur côté client.

    location / {
        try_files $uri $uri/ /index.html;
    }

La différence avec le tutoriel précédent tient au dernier paramètre : au lieu de =404, on renvoie /index.html. Nginx cherche d’abord le fichier exact, puis le dossier, et à défaut sert l’application, qui affichera la bonne vue. C’est l’unique réglage qui distingue un hébergement de site statique classique d’un hébergement de SPA, et son oubli est l’une des premières incompréhensions des débutants qui déploient un build React.

Un mot enfin sur le tampon (buffering). Par défaut, Nginx met en mémoire tampon la réponse du backend avant de la transmettre au client, ce qui protège l’application des clients lents. C’est généralement souhaitable. Mais pour un flux continu — téléchargement progressif, événements en temps réel (SSE) — ce tampon retarde l’envoi. On le désactive alors localement avec proxy_buffering off; dans le location concerné. À ne faire que là où c’est nécessaire : sur une API classique, garder le tampon actif est le bon choix, car il libère plus vite votre processus Node.

Point d’étape — Un rafraîchissement sur une route gérée par le client (par exemple /suivi/12345) affiche la bonne page au lieu d’une 404. Le fallback SPA fonctionne.

🐞 Pièges fréquents

Symptôme / erreur Cause probable Correctif
« 502 Bad Gateway » L’application n’écoute pas, ou pas sur le bon port Vérifier que Node tourne et écoute bien sur 127.0.0.1:3000
« 504 Gateway Timeout » Le backend met trop de temps à répondre Diagnostiquer la lenteur côté application ; ajuster proxy_read_timeout
Chemins d’API doublés (/api/api/...) Mauvaise gestion de la barre finale du proxy_pass Comprendre la règle de la barre oblique finale (étape 2)
Redirections en boucle en HTTPS X-Forwarded-Proto absent Ajouter proxy_set_header X-Forwarded-Proto $scheme;
WebSocket qui se déconnecte En-têtes Upgrade/Connection manquants Ajouter le bloc WebSocket de l’étape 4

🌍 Adaptation au contexte ouest-africain

Sur un VPS modeste, le reverse proxy est aussi un filet de sécurité économique : Nginx encaisse les connexions lentes et les clients capricieux à la place de votre application Node, qui reste libre de traiter la logique métier. Concrètement, une page front lourde chargée sur une 3G instable mobilise Nginx, pas votre processus Node ni votre base de données. Pensez aussi à ne jamais ouvrir le port applicatif sur le pare-feu : avec ufw, n’autorisez que OpenSSH et 'Nginx Full'. Tout le reste — y compris le 3000 — reste fermé de l’extérieur, et c’est très bien ainsi.

✅ Récapitulatif

Vous savez désormais placer une application derrière Nginx avec proxy_pass, lui transmettre les bons en-têtes pour qu’elle connaisse le vrai client, gérer les WebSockets, et diagnostiquer les erreurs 502/504. Votre front et votre API vivent sous un même domaine, le port interne reste caché. Il manque encore une chose essentielle pour la production : le chiffrement. C’est l’objet du tutoriel suivant.

🧾 Aide-mémoire

Directive Rôle
proxy_pass http://127.0.0.1:3000/; Relayer vers l’application interne
proxy_set_header Host $host; Transmettre le nom de domaine demandé
proxy_set_header X-Forwarded-For ... Transmettre l’IP réelle du client
proxy_set_header X-Forwarded-Proto $scheme; Indiquer le protocole d’origine
proxy_http_version 1.1; + Upgrade Autoriser les WebSockets

💪 À vous de jouer

Ajoutez un second service interne — par exemple un tableau de bord d’administration qui tourne sur le port 4000 — et exposez-le sous /admin/, tout en gardant l’API sous /api/. Les deux doivent cohabiter dans le même bloc server.

Voir une solution
    location /admin/ {
        proxy_pass http://127.0.0.1:4000/;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

Chaque location route vers un backend différent selon le préfixe d’URL. C’est ainsi qu’on assemble plusieurs microservices derrière une façade unique. Pour factoriser les en-têtes répétés, on peut les sortir dans un fichier snippets/ chargé par include.

Tutoriels frères

Pour aller plus loin

FAQ

Pourquoi une « 502 Bad Gateway » et pas une page d’erreur Node ?
Une 502 signifie que Nginx n’a pas pu joindre le backend du tout : l’application est arrêtée, écoute sur un autre port, ou a planté. L’erreur vient de Nginx, qui n’a personne à qui parler. La première chose à vérifier est que le processus tourne bien.

Faut-il un sous-domaine séparé pour l’API ?
Pas obligatoirement. Un préfixe de chemin (/api/) suffit et simplifie la configuration TLS. Un sous-domaine (api.colis-express.net) se justifie si vous voulez des règles, des certificats ou un hébergement distincts. Les deux approches sont valides.

Le reverse proxy ralentit-il les requêtes ?
La surcharge est négligeable — quelques fractions de milliseconde sur la boucle locale — et largement compensée par ce que Nginx apporte : gestion des connexions lentes, cache, compression, terminaison TLS. Le gain net est très positif.

Dois-je gérer le CORS dans Nginx ou dans l’application ?
Les deux sont possibles, mais évitez de le faire aux deux endroits à la fois : des en-têtes Access-Control-Allow-Origin en double cassent la requête côté navigateur. Quand le front et l’API partagent le même domaine via un préfixe comme /api/, le problème ne se pose souvent même pas, puisqu’il n’y a plus de requête inter-origine. C’est un avantage discret mais réel de servir front et API derrière un seul Nginx.

Partager