Node.js / Express / MongoDB Stack MERN
Serveur Node.js, API REST avec Express, base de données MongoDB et Mongoose ODM.
Node.js — Bases
Modules natifs — events, process, util
JS
// Variables globales Node.js
console.log(__filename); // Chemin absolu du fichier courant
console.log(__dirname); // Dossier du fichier courant
console.log(process.env.NODE_ENV); // Variables d'environnement
console.log(process.argv); // Arguments de la ligne de commande
process.exit(0); // Quitter le processus (0 = succès)
process.exit(1); // Quitter avec erreur
// EventEmitter
const EventEmitter = require('events');
class MonEmitter extends EventEmitter {}
const emitter = new MonEmitter();
emitter.on('message', (data) => console.log('Reçu:', data));
emitter.emit('message', { texte: 'Bonjour !' });
emitter.once('connect', () => console.log('Connecté une fois'));
emitter.removeAllListeners('message');
// Timers
setTimeout(() => console.log('Après 2 secondes'), 2000);
const interval = setInterval(() => console.log('Toutes les secondes'), 1000);
clearInterval(interval);
setImmediate(() => console.log('Prochaine itération event loop'));
process.nextTick(() => console.log('Avant les I/O'));
npm & package.json
Commandes npm essentielles
BASH
# Initialiser un projet
npm init -y # package.json par défaut
# Installer des packages
npm install express # dépendance de prod
npm install -D nodemon jest # dépendance de dev
npm install -g pm2 # installation globale
npm install express mongoose dotenv cors bcryptjs jsonwebtoken
# Mettre à jour / supprimer
npm update express
npm uninstall express
npm list # lister les packages
npm list --depth=0 # uniquement les directs
npm outdated # packages obsolètes
# Scripts
npm run dev
npm run build
npm test
npm start
package.json — configuration complète
JSON
{
"name": "mon-api",
"version": "1.0.0",
"description": "API REST Node.js / Express / MongoDB",
"main": "server.js",
"type": "module",
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js",
"test": "jest --watchAll",
"lint": "eslint ."
},
"dependencies": {
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
"dotenv": "^16.0.0",
"express": "^4.18.2",
"jsonwebtoken": "^9.0.0",
"mongoose": "^8.0.0",
"morgan": "^1.10.0"
},
"devDependencies": {
"jest": "^29.0.0",
"nodemon": "^3.0.0"
}
}
Modules CommonJS / ESM
require vs import/export
JS
// ===== CommonJS (par défaut, "type": "commonjs") =====
const express = require('express');
const path = require('path');
const { readFile } = require('fs/promises');
const MonModule = require('./mon-module');
module.exports = { maFonction, maConstante };
module.exports = MaClasse; // export par défaut
// ===== ES Modules ("type": "module" dans package.json) =====
import express from 'express';
import { readFile, writeFile } from 'fs/promises';
import path from 'path';
import { fileURLToPath } from 'url';
// __dirname équivalent en ESM
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Export
export const maConstante = 42;
export function maFonction() {}
export default class MaClasse {}
// Import dynamique
const module = await import('./module.js');
Fichiers & Chemins
Module fs, path
JS
import { readFile, writeFile, appendFile, unlink, mkdir, readdir, stat } from 'fs/promises';
import { existsSync } from 'fs';
import path from 'path';
// Lire un fichier
const contenu = await readFile('./data.json', 'utf-8');
const data = JSON.parse(contenu);
// Écrire un fichier
await writeFile('./output.json', JSON.stringify(data, null, 2), 'utf-8');
// Ajouter au fichier
await appendFile('./log.txt', `${new Date().toISOString()} - Action\n`);
// Supprimer
await unlink('./fichier.tmp');
// Créer un dossier
await mkdir('./uploads', { recursive: true });
// Lister un dossier
const fichiers = await readdir('./public');
// Infos d'un fichier
const infos = await stat('./fichier.txt');
infos.size; // taille en octets
infos.mtime; // date de modification
infos.isDirectory(); // true/false
// Vérifier l'existence
if (existsSync('./config.json')) { ... }
// ===== PATH =====
path.join(__dirname, 'public', 'index.html'); // chemin sécurisé
path.resolve('./public'); // chemin absolu
path.basename('/chemin/vers/fichier.txt'); // 'fichier.txt'
path.dirname('/chemin/vers/fichier.txt'); // '/chemin/vers'
path.extname('fichier.txt'); // '.txt'
path.parse('/chemin/fichier.txt');
// { root: '/', dir: '/chemin', base: 'fichier.txt', ext: '.txt', name: 'fichier' }
Express — Setup
Configuration d'un serveur Express complet
JS
// server.js
import express from 'express';
import cors from 'cors';
import morgan from 'morgan';
import dotenv from 'dotenv';
import connectDB from './config/db.js';
import userRoutes from './routes/users.js';
import articleRoutes from './routes/articles.js';
import { errorHandler, notFound } from './middleware/error.js';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
// ===== CONNEXION BASE DE DONNÉES =====
connectDB();
// ===== MIDDLEWARE GLOBAUX =====
app.use(cors({
origin: process.env.CLIENT_URL || 'http://localhost:3000',
credentials: true,
}));
app.use(morgan('dev')); // logs HTTP
app.use(express.json()); // parse JSON body
app.use(express.urlencoded({ extended: true })); // parse form data
app.use(express.static('public')); // fichiers statiques
// ===== ROUTES =====
app.get('/', (req, res) => res.json({ message: 'API ThinkSmart v1' }));
app.use('/api/users', userRoutes);
app.use('/api/articles', articleRoutes);
// ===== GESTION DES ERREURS (toujours en dernier) =====
app.use(notFound);
app.use(errorHandler);
// ===== DÉMARRAGE =====
app.listen(PORT, () => {
console.log(`✅ Serveur démarré sur http://localhost:${PORT}`);
});
Routes & Paramètres
Router Express — tous les cas
JS
// routes/articles.js
import express from 'express';
import { protect } from '../middleware/auth.js';
import {
getArticles, getArticle, createArticle, updateArticle, deleteArticle
} from '../controllers/articleController.js';
const router = express.Router();
// Routes publiques
router.get('/', getArticles); // GET /api/articles
router.get('/:id', getArticle); // GET /api/articles/64abc123
// Routes protégées (nécessitent un token JWT)
router.post('/', protect, createArticle); // POST /api/articles
router.put('/:id', protect, updateArticle); // PUT /api/articles/64abc123
router.delete('/:id', protect, deleteArticle); // DELETE /api/articles/64abc123
// Chainable
router.route('/')
.get(getArticles)
.post(protect, createArticle);
router.route('/:id')
.get(getArticle)
.put(protect, updateArticle)
.delete(protect, deleteArticle);
export default router;
// ===== PARAMÈTRES =====
// Route params : /articles/:id/:slug
app.get('/articles/:id', (req, res) => {
const { id } = req.params; // { id: "64abc123" }
});
// Query string : /articles?page=2&limit=10&sort=date
app.get('/articles', (req, res) => {
const { page = 1, limit = 10, sort = 'date' } = req.query;
});
// Route avec regex
app.get('/articles/:id(\\d+)', handler); // id numérique seulement
app.get('/assets/*', handler); // wildcard
Middleware
Créer des middleware personnalisés
Un middleware est une fonction (req, res, next). Il peut modifier req/res, terminer la requête ou appeler next() pour passer au suivant.
JS
// middleware/logger.js — Logger custom
export const logger = (req, res, next) => {
console.log(`${new Date().toISOString()} ${req.method} ${req.url}`);
next(); // OBLIGATOIRE pour continuer
};
// middleware/auth.js — Vérification JWT
import jwt from 'jsonwebtoken';
import User from '../models/User.js';
export const protect = async (req, res, next) => {
let token;
if (req.headers.authorization?.startsWith('Bearer ')) {
token = req.headers.authorization.split(' ')[1];
}
if (!token) {
return res.status(401).json({ message: 'Non autorisé — token manquant' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = await User.findById(decoded.id).select('-password');
next();
} catch {
res.status(401).json({ message: 'Token invalide' });
}
};
// middleware/admin.js — Vérification du rôle
export const adminOnly = (req, res, next) => {
if (req.user?.role !== 'admin') {
return res.status(403).json({ message: 'Accès refusé — admin requis' });
}
next();
};
// middleware/validate.js — Validation des données
export const validate = (schema) => (req, res, next) => {
const { error } = schema.validate(req.body);
if (error) {
return res.status(400).json({ message: error.details[0].message });
}
next();
};
// middleware/rateLimit.js — Limitation du débit
import rateLimit from 'express-rate-limit';
export const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // max 100 requêtes par fenêtre
message: 'Trop de requêtes, réessayez dans 15 minutes.',
});
// Middleware d'erreur (4 paramètres obligatoires)
export const errorHandler = (err, req, res, next) => {
const status = err.status || 500;
res.status(status).json({
message: err.message || 'Erreur serveur',
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined,
});
};
export const notFound = (req, res) => {
res.status(404).json({ message: `Route ${req.url} non trouvée` });
};
Request & Response
Objet req et res — toutes les propriétés/méthodes
JS
app.all('/exemple', (req, res) => {
// ===== REQUÊTE (req) =====
req.method; // "GET", "POST", "PUT", "DELETE"
req.url; // "/api/users?page=2"
req.path; // "/api/users"
req.originalUrl; // URL originale avant le montage
req.params; // { id: "42" } — paramètres de route
req.query; // { page: "2", limit: "10" } — query string
req.body; // { nom: "Alice" } — body JSON (après express.json())
req.headers; // { authorization: "Bearer ...", ... }
req.headers['content-type']; // "application/json"
req.get('Authorization'); // raccourci header
req.ip; // IP du client
req.hostname; // nom d'hôte
req.protocol; // "http" ou "https"
req.secure; // true si HTTPS
req.cookies; // { session: "..." } — avec cookie-parser
req.file; // fichier uploadé (multer)
req.files; // plusieurs fichiers
req.user; // utilisateur (ajouté par middleware d'auth)
// ===== RÉPONSE (res) =====
// Statuts courants : 200 OK, 201 Created, 204 No Content,
// 400 Bad Request, 401 Unauthorized, 403 Forbidden,
// 404 Not Found, 409 Conflict, 422 Unprocessable, 500 Server Error
// JSON
res.status(200).json({ success: true, data: users });
res.status(201).json({ message: 'Créé', data: newUser });
res.status(204).send(); // No Content (DELETE)
// Texte
res.send('Bonjour');
res.status(404).send('Non trouvé');
// HTML
res.sendFile(path.join(__dirname, 'public', 'index.html'));
// Headers
res.set('Content-Type', 'application/json');
res.set('X-Custom-Header', 'valeur');
// Redirection
res.redirect('/nouveau-chemin');
res.redirect(301, '/permanent');
// Cookie
res.cookie('session', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 jours
});
res.clearCookie('session');
});
MongoDB — Bases
Connexion & commandes MongoDB shell
JS
// config/db.js — Connexion Mongoose
import mongoose from 'mongoose';
const connectDB = async () => {
try {
const conn = await mongoose.connect(process.env.MONGO_URI);
console.log(`✅ MongoDB connecté : ${conn.connection.host}`);
} catch (err) {
console.error('❌ Erreur MongoDB:', err.message);
process.exit(1);
}
};
export default connectDB;
// .env
// MONGO_URI=mongodb://localhost:27017/ma_base
// MONGO_URI=mongodb+srv://user:password@cluster.mongodb.net/ma_base
BASH
# MongoDB Shell (mongosh)
mongosh # se connecter localement
use ma_base # créer/utiliser une base
show dbs # lister les bases
show collections # lister les collections
# CRUD basique en shell
db.users.insertOne({ nom: "Alice", email: "alice@ex.fr" })
db.users.insertMany([{ nom: "Bob" }, { nom: "Charlie" }])
db.users.find()
db.users.find({ nom: "Alice" })
db.users.findOne({ email: "alice@ex.fr" })
db.users.updateOne({ nom: "Alice" }, { $set: { age: 30 } })
db.users.deleteOne({ nom: "Alice" })
db.users.countDocuments({ actif: true })
db.users.drop() # supprimer la collection
Mongoose — Schémas & Modèles
Définir un schéma Mongoose complet
JS
// models/User.js
import mongoose from 'mongoose';
import bcrypt from 'bcryptjs';
const userSchema = new mongoose.Schema(
{
nom: {
type: String,
required: [true, 'Le nom est obligatoire'],
trim: true,
minlength: [2, 'Minimum 2 caractères'],
maxlength: [50, 'Maximum 50 caractères'],
},
email: {
type: String,
required: [true, "L'email est obligatoire"],
unique: true,
lowercase: true,
trim: true,
match: [/^\S+@\S+\.\S+$/, 'Email invalide'],
},
password: {
type: String,
required: true,
minlength: 8,
select: false, // exclu par défaut des requêtes
},
role: {
type: String,
enum: ['user', 'admin', 'moderateur'],
default: 'user',
},
actif: { type: Boolean, default: true },
age: { type: Number, min: 0, max: 150 },
bio: { type: String, default: null },
avatar: {
type: String,
default: 'default-avatar.png',
},
// Référence vers une autre collection
articles: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Article' }],
},
{
timestamps: true, // ajoute createdAt et updatedAt automatiquement
toJSON: { virtuals: true },
toObject: { virtuals: true },
}
);
// ===== VIRTUAL (propriété calculée, non stockée) =====
userSchema.virtual('nomComplet').get(function() {
return `${this.prenom} ${this.nom}`;
});
// ===== MIDDLEWARE (hooks) =====
// Avant la sauvegarde : hasher le mot de passe
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(12);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// ===== MÉTHODES D'INSTANCE =====
userSchema.methods.comparePassword = async function(plainPassword) {
return bcrypt.compare(plainPassword, this.password);
};
userSchema.methods.toPublic = function() {
const obj = this.toObject();
delete obj.password;
return obj;
};
// ===== MÉTHODES STATIQUES =====
userSchema.statics.findByEmail = function(email) {
return this.findOne({ email: email.toLowerCase() });
};
// ===== INDEX =====
userSchema.index({ email: 1 }, { unique: true });
userSchema.index({ createdAt: -1 });
const User = mongoose.model('User', userSchema);
export default User;
Modèle Article avec population
JS
// models/Article.js
import mongoose from 'mongoose';
const articleSchema = new mongoose.Schema({
titre: {
type: String,
required: [true, 'Titre obligatoire'],
trim: true,
maxlength: 200,
},
slug: {
type: String,
unique: true,
},
contenu: { type: String, required: true },
resume: { type: String, maxlength: 500 },
publie: { type: Boolean, default: false },
vues: { type: Number, default: 0 },
tags: [String],
// Référence à l'auteur
auteur: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true,
},
// Commentaires embarqués (subdocuments)
commentaires: [{
user: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
texte: { type: String, required: true },
date: { type: Date, default: Date.now },
}],
}, { timestamps: true });
// Générer le slug avant sauvegarde
articleSchema.pre('save', function(next) {
if (this.isModified('titre')) {
this.slug = this.titre.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '');
}
next();
});
const Article = mongoose.model('Article', articleSchema);
export default Article;
Mongoose — CRUD & Requêtes
Toutes les méthodes Mongoose
JS
import Article from '../models/Article.js';
import User from '../models/User.js';
// ===== CREATE =====
const article = new Article({ titre: 'Mon article', auteur: userId });
await article.save();
// Raccourci
const article = await Article.create({
titre: 'Mon article',
contenu: 'Contenu...',
auteur: req.user._id,
});
// Plusieurs
await Article.insertMany([{ titre: 'A1' }, { titre: 'A2' }]);
// ===== READ =====
const articles = await Article.find();
const articles = await Article.find({ publie: true });
// Sélectionner les champs
await Article.find().select('titre resume createdAt');
await Article.find().select('-contenu'); // exclure
// Population (jointure)
await Article.find().populate('auteur', 'nom email');
await Article.findById(id)
.populate('auteur', 'nom email avatar')
.populate('commentaires.user', 'nom');
// Tri, pagination
await Article.find({ publie: true })
.sort({ createdAt: -1 }) // décroissant
.sort({ vues: -1, createdAt: -1 }) // multi-critères
.skip((page - 1) * limit)
.limit(limit);
// Filtres avancés
await Article.find({
publie: true,
vues: { $gt: 100 }, // supérieur à
tags: { $in: ['javascript', 'nodejs'] }, // contient l'un des tags
titre: { $regex: query, $options: 'i' }, // like, insensible casse
createdAt: { $gte: new Date('2024-01-01') },
});
// Compter
const total = await Article.countDocuments({ publie: true });
// findOne
const article = await Article.findOne({ slug: 'mon-article' });
const article = await Article.findById(id); // équiv. findOne({ _id: id })
// ===== UPDATE =====
// Trouver et modifier (retourne le document MIS À JOUR)
const updated = await Article.findByIdAndUpdate(
id,
{ $set: { titre: 'Nouveau titre', publie: true } },
{ new: true, runValidators: true }
);
// Opérateurs de mise à jour MongoDB
await Article.findByIdAndUpdate(id, {
$set: { titre: 'Nouveau' }, // définir un champ
$unset: { resume: '' }, // supprimer un champ
$inc: { vues: 1 }, // incrémenter
$push: { tags: 'nouveau-tag' }, // ajouter à un tableau
$pull: { tags: 'ancien-tag' }, // retirer d'un tableau
$addToSet: { tags: 'tag-unique' }, // ajouter si absent
});
// Mise à jour multiple
await Article.updateMany({ publie: false }, { $set: { publie: true } });
// Upsert (créer si n'existe pas)
await Article.findOneAndUpdate(
{ slug: 'mon-article' },
{ titre: 'Titre' },
{ upsert: true, new: true }
);
// ===== DELETE =====
await Article.findByIdAndDelete(id);
await Article.deleteMany({ publie: false, vues: 0 });
// ===== AGGREGATION =====
const stats = await Article.aggregate([
{ $match: { publie: true } },
{ $group: { _id: '$auteur', totalArticles: { $sum: 1 }, totalVues: { $sum: '$vues' } } },
{ $sort: { totalVues: -1 } },
{ $limit: 10 },
{ $lookup: { from: 'users', localField: '_id', foreignField: '_id', as: 'auteur' } },
{ $unwind: '$auteur' },
{ $project: { 'auteur.password': 0 } },
]);
API CRUD complète
Controller CRUD — Pattern complet
JS
// controllers/articleController.js
import Article from '../models/Article.js';
// @desc Récupérer tous les articles
// @route GET /api/articles
// @access Public
export const getArticles = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const sort = req.query.sort || '-createdAt';
const search = req.query.search;
const skip = (page - 1) * limit;
const filter = { publie: true };
if (search) filter.titre = { $regex: search, $options: 'i' };
const [articles, total] = await Promise.all([
Article.find(filter)
.populate('auteur', 'nom avatar')
.sort(sort)
.skip(skip)
.limit(limit)
.select('-contenu'),
Article.countDocuments(filter),
]);
res.json({
success: true,
data: articles,
total,
page,
pages: Math.ceil(total / limit),
});
} catch (err) {
res.status(500).json({ success: false, message: err.message });
}
};
// @desc Récupérer un article par ID
// @route GET /api/articles/:id
// @access Public
export const getArticle = async (req, res) => {
try {
const article = await Article.findById(req.params.id)
.populate('auteur', 'nom email avatar');
if (!article) {
return res.status(404).json({ message: 'Article non trouvé' });
}
// Incrémenter les vues
await Article.findByIdAndUpdate(req.params.id, { $inc: { vues: 1 } });
res.json({ success: true, data: article });
} catch (err) {
if (err.name === 'CastError') {
return res.status(400).json({ message: 'ID invalide' });
}
res.status(500).json({ message: err.message });
}
};
// @desc Créer un article
// @route POST /api/articles
// @access Private
export const createArticle = async (req, res) => {
try {
const { titre, contenu, tags, resume } = req.body;
const article = await Article.create({
titre,
contenu,
tags,
resume,
auteur: req.user._id,
});
const populated = await article.populate('auteur', 'nom email');
res.status(201).json({ success: true, data: populated });
} catch (err) {
if (err.name === 'ValidationError') {
const messages = Object.values(err.errors).map(e => e.message);
return res.status(400).json({ message: messages.join(', ') });
}
if (err.code === 11000) {
return res.status(409).json({ message: 'Cet article existe déjà' });
}
res.status(500).json({ message: err.message });
}
};
// @desc Modifier un article
// @route PUT /api/articles/:id
// @access Private
export const updateArticle = async (req, res) => {
try {
let article = await Article.findById(req.params.id);
if (!article) return res.status(404).json({ message: 'Non trouvé' });
// Vérifier que l'utilisateur est l'auteur ou admin
if (article.auteur.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ message: 'Non autorisé' });
}
article = await Article.findByIdAndUpdate(
req.params.id,
{ $set: req.body },
{ new: true, runValidators: true }
).populate('auteur', 'nom email');
res.json({ success: true, data: article });
} catch (err) {
res.status(500).json({ message: err.message });
}
};
// @desc Supprimer un article
// @route DELETE /api/articles/:id
// @access Private
export const deleteArticle = async (req, res) => {
try {
const article = await Article.findById(req.params.id);
if (!article) return res.status(404).json({ message: 'Non trouvé' });
if (article.auteur.toString() !== req.user._id.toString() && req.user.role !== 'admin') {
return res.status(403).json({ message: 'Non autorisé' });
}
await article.deleteOne();
res.status(204).send();
} catch (err) {
res.status(500).json({ message: err.message });
}
};
Authentification JWT
Register, Login, Token — pattern complet
JS
// controllers/authController.js
import jwt from 'jsonwebtoken';
import User from '../models/User.js';
// Générer un token JWT
const generateToken = (userId) => {
return jwt.sign(
{ id: userId },
process.env.JWT_SECRET,
{ expiresIn: process.env.JWT_EXPIRES_IN || '7d' }
);
};
// Envoyer le token en cookie + JSON
const sendToken = (user, statusCode, res) => {
const token = generateToken(user._id);
res.cookie('token', token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.status(statusCode).json({
success: true,
token,
user: { id: user._id, nom: user.nom, email: user.email, role: user.role },
});
};
// @route POST /api/auth/register
export const register = async (req, res) => {
try {
const { nom, email, password } = req.body;
if (await User.findOne({ email })) {
return res.status(409).json({ message: 'Email déjà utilisé' });
}
const user = await User.create({ nom, email, password });
sendToken(user, 201, res);
} catch (err) {
if (err.name === 'ValidationError') {
return res.status(400).json({ message: Object.values(err.errors).map(e => e.message).join(', ') });
}
res.status(500).json({ message: err.message });
}
};
// @route POST /api/auth/login
export const login = async (req, res) => {
try {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ message: 'Email et mot de passe requis' });
}
// select('+password') car le champ est select: false dans le schéma
const user = await User.findOne({ email }).select('+password');
if (!user || !(await user.comparePassword(password))) {
return res.status(401).json({ message: 'Identifiants invalides' });
}
if (!user.actif) {
return res.status(403).json({ message: 'Compte désactivé' });
}
sendToken(user, 200, res);
} catch (err) {
res.status(500).json({ message: err.message });
}
};
// @route GET /api/auth/me
export const getMe = async (req, res) => {
const user = await User.findById(req.user._id);
res.json({ success: true, data: user });
};
// @route POST /api/auth/logout
export const logout = (req, res) => {
res.cookie('token', '', { httpOnly: true, expires: new Date(0) });
res.json({ success: true, message: 'Déconnecté' });
};
// routes/auth.js
import { register, login, getMe, logout } from '../controllers/authController.js';
import { protect } from '../middleware/auth.js';
const router = express.Router();
router.post('/register', register);
router.post('/login', login);
router.get('/me', protect, getMe);
router.post('/logout', protect, logout);
Gestion des erreurs
Classe d'erreur custom + middleware centralisé
JS
// utils/AppError.js
export class AppError extends Error {
constructor(message, statusCode) {
super(message);
this.statusCode = statusCode;
this.isOperational = true; // distingue les erreurs "métier" des bugs
Error.captureStackTrace(this, this.constructor);
}
}
// middleware/error.js
import { AppError } from '../utils/AppError.js';
export const errorHandler = (err, req, res, next) => {
let error = { ...err, message: err.message };
// Erreur ID MongoDB invalide
if (err.name === 'CastError') {
error = new AppError('ID invalide', 400);
}
// Doublon (champ unique)
if (err.code === 11000) {
const field = Object.keys(err.keyValue)[0];
error = new AppError(`${field} déjà utilisé`, 409);
}
// Erreur de validation Mongoose
if (err.name === 'ValidationError') {
const msg = Object.values(err.errors).map(e => e.message).join(', ');
error = new AppError(msg, 400);
}
// Token JWT invalide
if (err.name === 'JsonWebTokenError') {
error = new AppError('Token invalide', 401);
}
if (err.name === 'TokenExpiredError') {
error = new AppError('Token expiré', 401);
}
res.status(error.statusCode || 500).json({
success: false,
message: error.message || 'Erreur serveur',
...(process.env.NODE_ENV === 'development' && { stack: err.stack }),
});
};
// Wrapper async pour éviter try/catch répétitif
export const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
// Utilisation avec asyncHandler
import { asyncHandler } from '../middleware/error.js';
export const getArticle = asyncHandler(async (req, res) => {
const article = await Article.findById(req.params.id);
if (!article) throw new AppError('Article non trouvé', 404);
res.json({ success: true, data: article });
});
Variables d'environnement & Configuration
dotenv — fichier .env complet
ENV
# .env (jamais versionné — dans .gitignore)
# Serveur
NODE_ENV=development
PORT=5000
# Base de données
MONGO_URI=mongodb://localhost:27017/thinksmart_dev
# JWT
JWT_SECRET=un_secret_tres_long_et_aleatoire_ici_minimum_32_chars
JWT_EXPIRES_IN=7d
# Client (CORS)
CLIENT_URL=http://localhost:3000
# Email
MAIL_HOST=smtp.mailtrap.io
MAIL_PORT=2525
MAIL_USER=votre_user
MAIL_PASS=votre_pass
MAIL_FROM=noreply@thinksmart.fr
# Cloudinary (upload images)
CLOUDINARY_CLOUD_NAME=votre_cloud
CLOUDINARY_API_KEY=votre_key
CLOUDINARY_API_SECRET=votre_secret
JS
// config/index.js — Centraliser la config
import dotenv from 'dotenv';
dotenv.config();
export default {
env: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT) || 5000,
mongoUri: process.env.MONGO_URI,
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
clientUrl: process.env.CLIENT_URL || 'http://localhost:3000',
isDev: process.env.NODE_ENV === 'development',
isProd: process.env.NODE_ENV === 'production',
};
// Utilisation
import config from './config/index.js';
console.log(config.port); // 5000
console.log(config.jwt.secret); // votre secret
Aucun résultat pour votre recherche.