Si tu as déjà développé une API NestJS consommée par un frontend Next.js, React ou Vue, tu as forcément croisé ce message d'erreur frustrant dans la console du navigateur : "Access to fetch at ... has been blocked by CORS policy". Cette erreur, loin d'être un bug, est en réalité une protection essentielle du navigateur. Dans ce tutoriel, nous allons voir comment configurer CORS proprement dans une application NestJS, en s'appuyant sur une configuration réelle issue d'un projet en production (un site de voyance avec frontend Next.js, backend NestJS et intégration Stripe).
Qu'est-ce que CORS et pourquoi ça bloque mes requêtes ?
CORS (Cross-Origin Resource Sharing) est un mécanisme de sécurité implémenté par les navigateurs. Par défaut, ils appliquent la Same-Origin Policy : un script chargé depuis https://monsite.com ne peut pas faire de requêtes AJAX vers https://api.autredomaine.com sans autorisation explicite du serveur cible.
Une origine est définie par le triplet protocole + domaine + port. Ainsi, http://localhost:3000 et http://localhost:3002 sont considérés comme deux origines différentes, même s'ils tournent sur la même machine. C'est exactement la situation typique d'un développement local : le frontend tourne sur le port 3000, l'API NestJS sur le port 3002.
Quand ton frontend appelle l'API, le navigateur ajoute automatiquement un header Origin à la requête. Le serveur doit répondre avec les bons headers Access-Control-Allow-* pour autoriser l'échange. Sans cela, le navigateur bloque la réponse, même si l'API a bien traité la requête côté serveur.
Activer CORS dans NestJS avec enableCors()
NestJS expose une méthode dédiée sur l'instance de l'application : app.enableCors(). Dans le fichier main.ts, c'est l'endroit idéal pour la configurer, juste après la création de l'application :
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// Configuration CORS la plus simple
app.enableCors();
await app.listen(3002);
}
bootstrap();Appelée sans argument, cette méthode autorise toutes les origines. C'est pratique en développement, mais dangereux en production : n'importe quel site malveillant pourrait alors faire des requêtes vers ton API depuis le navigateur d'un utilisateur authentifié. Il faut donc paramétrer finement cette configuration.
Les options de configuration essentielles
Voici la configuration utilisée dans le projet de référence, qui couvre les besoins concrets d'une API moderne :
app.enableCors({
origin: origins,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'stripe-signature'],
credentials: true,
});Décortiquons chaque option :
- origin : la ou les origines autorisées. Peut être une chaîne, un tableau, une regex, ou une fonction.
- methods : les méthodes HTTP autorisées. Inclure
OPTIONSest crucial pour les requêtes préflight (voir plus bas). - allowedHeaders : les headers que le client peut envoyer. Si tu utilises JWT,
Authorizationest obligatoire. - credentials : autorise l'envoi de cookies et headers d'authentification cross-origin.
Gérer plusieurs origines via les variables d'environnement
En pratique, une API doit souvent accepter plusieurs origines : un frontend public, un dashboard admin, un environnement de staging... Hardcoder ces URLs dans le code est une mauvaise pratique. La bonne approche consiste à les externaliser dans une variable d'environnement.
Dans le fichier configuration.ts du projet, on parse une variable CORS_ORIGINS contenant une liste séparée par des virgules :
export default () => ({
port: parseInt(process.env.PORT || '3002', 10),
cors: {
origin: process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(',').map(o => o.trim())
: ['http://localhost:3000', 'http://localhost:3004'], // Frontend + Admin
},
// ...
});Côté main.ts, on récupère cette configuration via le ConfigService et on s'assure de toujours obtenir un tableau, peu importe le format reçu :
const configService = app.get(ConfigService);
const logger = new Logger('Bootstrap');
const corsOrigins = configService.get<string[] | string>('cors.origin');
const origins = Array.isArray(corsOrigins)
? corsOrigins
: (typeof corsOrigins === 'string'
? corsOrigins.split(',').map(o => o.trim())
: ['http://localhost:3000']);
logger.log(`CORS origins: ${JSON.stringify(origins)}`);
app.enableCors({
origin: origins,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'stripe-signature'],
credentials: true,
});Le fichier .env ressemble alors à :
CORS_ORIGINS=https://monsite.com,https://admin.monsite.com,https://staging.monsite.comLe logger.log affichant les origines au démarrage est précieux : il te permet de vérifier au lancement que la configuration chargée est bien celle attendue, ce qui évite des heures de debug.
Headers spéciaux : Authorization et stripe-signature
Par défaut, CORS n'autorise qu'un nombre limité de headers dits « sûrs » (Accept, Content-Type avec certaines valeurs, etc.). Tout header personnalisé doit être déclaré explicitement dans allowedHeaders.
Deux headers méritent une attention particulière dans le projet :
- Authorization : indispensable pour transporter un token JWT (
Bearer eyJhbGc...). Sans ce header dans la liste, toutes les requêtes authentifiées seront bloquées par le navigateur. - stripe-signature : utilisé par les webhooks Stripe pour signer cryptographiquement les notifications. Bien que les webhooks soient généralement des requêtes serveur-à-serveur (non concernées par CORS), l'autoriser permet aussi des tests depuis des outils navigateur.
Si tu oublies un header, tu verras une erreur du type : "Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response."
Credentials : cookies et authentification cross-origin
L'option credentials: true est nécessaire si tu veux que le navigateur transmette les cookies, les headers d'authentification HTTP basique, ou les certificats client lors d'une requête cross-origin. C'est typiquement le cas si tu utilises des cookies httpOnly pour stocker les tokens de session.
Attention à un piège : quand credentials est à true, l'option origin ne peut pas être '*'. Le navigateur exige une origine explicite. C'est pour cette raison qu'on liste précisément les domaines autorisés.
Côté frontend, il faut aussi activer l'envoi des credentials. Avec fetch :
fetch('https://api.monsite.com/bookings', {
method: 'POST',
credentials: 'include', // <-- indispensable
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify(data),
});Les requêtes préflight : ce que fait le navigateur en coulisses
Pour les requêtes considérées comme « non simples » (méthode PUT, DELETE, PATCH, headers personnalisés, Content-Type JSON, etc.), le navigateur effectue d'abord une requête OPTIONS appelée preflight. Cette requête demande au serveur : « Est-ce que tu autorises bien cette origine à faire un PUT avec ces headers ? »
Le serveur doit répondre avec les bons headers Access-Control-Allow-*. Si la réponse est positive, le navigateur envoie la vraie requête. Sinon, il la bloque sans même l'envoyer.
C'est pour cette raison que 'OPTIONS' figure dans la liste des methods autorisées. NestJS gère automatiquement la réponse aux préflights dès que enableCors() est appelé.
Debugger les erreurs CORS
Voici les erreurs les plus courantes et leur cause :
- « No 'Access-Control-Allow-Origin' header » : ton origine n'est pas dans la liste autorisée. Vérifie la variable
CORS_ORIGINSet le log de démarrage. - « Request header field X is not allowed » : ajoute le header dans
allowedHeaders. - « Credentials flag is true, but Access-Control-Allow-Origin is '*' » : remplace
*par la liste explicite des origines. - « Method PUT is not allowed » : ajoute la méthode dans
methods.
Pour debugger efficacement, ouvre l'onglet Réseau des DevTools du navigateur et regarde la requête OPTIONS : ses headers de réponse t'indiquent exactement ce que le serveur autorise. Tu peux aussi tester avec curl en simulant une préflight :
curl -i -X OPTIONS https://api.monsite.com/bookings \
-H "Origin: https://monsite.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Authorization,Content-Type"Conclusion
Une bonne configuration CORS dans NestJS repose sur quelques règles simples : externaliser les origines dans une variable d'environnement, lister explicitement les méthodes et headers nécessaires, activer credentials uniquement quand c'est requis, et logger la configuration au démarrage pour faciliter le debug. La méthode app.enableCors() centralise tout cela proprement dans le main.ts.
Pour aller plus loin, tu peux explorer la configuration d'origines dynamiques via une fonction (utile pour autoriser des sous-domaines en wildcard), l'intégration avec un reverse proxy comme Traefik ou Nginx qui peut aussi gérer CORS, ou encore la configuration spécifique pour les WebSockets qui suivent leurs propres règles. La documentation officielle de NestJS détaille toutes les options disponibles.




Commentaires