Chapitre 169 minIntermédiaire
Aide-mémoire Node.js, npm et Express
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- retrouver vite une commande, une méthode ou un code de statut ;
- vérifier ton API avec la checklist avant une démonstration.
Cet aide-mémoire rassemble sur une page les commandes Node.js et npm, le squelette d'une API Express 5, les méthodes de req et res, l'accès à MySQL avec mysql2 et Prisma, la validation avec Zod, l'authentification par JWT, les codes de statut et la checklist à passer avant une démonstration. Chaque ligne renvoie au chapitre qui l'explique.
Commandes node et npm
| Commande | Effet | Chapitre |
|---|---|---|
node fichier.js | Exécute un script | 1 |
node --watch --env-file=.env src/server.js | Relance à chaque modification, avec les variables du .env | 2 |
node --run dev ou npm run dev | Lance le script dev du package.json | 2 |
node --test "test/**/*.test.js" | Lance les tests | 12 |
npm init -y puis npm pkg set type=module | Crée le projet en modules ES | 2 |
npm install paquet / npm install -D paquet | Dépendance d'exécution / de développement | 2 |
npm ci --omit=dev | Installation exacte, sans les outils de développement (production, CI) | 2 et 14 |
npm outdated, npm update, npm audit | Versions en retard, mise à jour compatible, vulnérabilités connues | 2 |
npx prisma@7 generate | Génère le client Prisma | 7 |
Le squelette de l'API
applifrais-api/
├── package.json, package-lock.json, .env.example, .gitignore, Dockerfile, compose.yaml
├── docs/openapi.yaml
├── sql/schema.sql, sql/donnees.sql
├── src/
│ ├── server.js démarrage, arrêt propre
│ ├── app.js middlewares et routeurs, dans l'ordre
│ ├── config.js variables d'environnement vérifiées
│ ├── db.js pool mysql2
│ ├── logger.js pino et pino-http
│ ├── erreurs.js ErreurApi, route 404, gestionnaire d'erreurs
│ ├── middlewares/ authentifier, exigerRole, valider, televersement
│ ├── schemas/ schémas Zod
│ ├── routes/ un routeur par ressource
│ ├── controllers/ HTTP : lire req, choisir la réponse
│ ├── services/ règles de gestion
│ └── depots/ SQL
└── test/ tests node:test et Supertest
// src/app.js : l'ordre compte
app.use(journalRequetes); // 1. journal
app.use(helmet()); // 2. en-têtes de sécurité
app.use(cors({ origin: config.origineFront })); // 3. CORS
app.use(express.json({ limit: '100kb' })); // 4. corps JSON
app.get('/sante', (req, res) => res.json({ statut: 'ok' }));
app.use('/v1/auth', routeurAuth); // 5. routes
app.use('/v1/fiches', routeurFiches);
app.use(routeInconnue); // 6. 404
app.use(gestionnaireErreurs); // 7. erreurs, quatre paramètres
req et res
| Lire la requête | Valeur |
|---|---|
req.params.id | Paramètre de chemin, chaîne |
req.query.etat | Chaîne de requête, chaîne ou tableau ; lecture seule en Express 5 |
req.body | Corps lu par express.json() ; undefined sans corps JSON |
req.get('Authorization') | Un en-tête |
req.ip, req.method, req.originalUrl | Adresse du client, méthode, adresse complète |
| Répondre | Exemple |
|---|---|
| Lecture | res.json(fiche) |
| Création | res.status(201).location(`/v1/fiches/${id}`).json(fiche) |
| Suppression | res.sendStatus(204) |
| Erreur | throw new ErreurApi(409, 'transition-interdite', "Changement d'état interdit", detail) |
| En-tête | res.set('Cache-Control', 'no-store') |
Express 5 : ce qui change
| Express 4 | Express 5 |
|---|---|
app.get('/docs/*', …) | app.get('/docs/*chemin', …) ; '/{*chemin}' inclut la racine |
'/rapports/:annee?' | '/rapports{/:annee}' |
try/catch + next(err) dans chaque route async | Inutile : les erreurs async vont au gestionnaire d'erreurs |
res.send(201), res.json(obj, 201) | res.sendStatus(201), res.status(201).json(obj) |
req.query modifiable, req.body vaut {} | req.query en lecture seule, req.body vaut undefined |
app.options('*', cors()) | Inutile (et plante) : app.use(cors(…)) suffit |
MySQL avec mysql2
import mysql from 'mysql2/promise';
export const pool = mysql.createPool({ ...config.db, connectionLimit: 10, decimalNumbers: true, dateStrings: true });
const [lignes] = await pool.execute('SELECT id, mois FROM fiche_frais WHERE id_visiteur = ?', [7]);
const [[{ total }]] = await pool.execute('SELECT COUNT(*) AS total FROM fiche_frais');
const [resultat] = await pool.execute('INSERT INTO … VALUES (?, ?)', [a, b]); // resultat.insertId
// UPDATE, DELETE : resultat.affectedRows
// LIMIT ? OFFSET ? : passer String(limite), String(decalage)
const connexion = await pool.getConnection();
try {
await connexion.beginTransaction();
// … connexion.execute(…) …
await connexion.commit();
} catch (erreur) {
await connexion.rollback();
throw erreur;
} finally {
connexion.release();
}
Erreurs à connaître : ER_DUP_ENTRY (1062) → 409 ; ER_NO_REFERENCED_ROW_2 (1452) ; ECONNREFUSED → MySQL injoignable (chapitre 6).
Prisma 7
await prisma.ficheFrais.findMany({ where: { etat: 'CL' }, orderBy: { mois: 'desc' }, skip: 0, take: 20 });
await prisma.ficheFrais.findUnique({ where: { id: 12 }, include: { lignesForfait: true } });
await prisma.ficheFrais.create({ data: { visiteurId: 7, mois: '2026-11' } });
await prisma.ficheFrais.updateMany({ where: { id: 12, etat: 'CR' }, data: { etat: 'CL' } }); // { count }
await prisma.$transaction([requete1, requete2]);
await prisma.$queryRaw`SELECT … WHERE id = ${id}`; // paramétré ; jamais $queryRawUnsafe avec une saisie
Codes : P2002 doublon, P2003 clé étrangère, P2025 introuvable. Installer avec la version : prisma@7, @prisma/client@7 (chapitre 7).
Zod 4
z.config(z.locales.fr());
z.strictObject({ mois: z.string().regex(/^\d{4}-(0[1-9]|1[0-2])$/) }); // corps : champs en trop refusés
z.object({ page: z.coerce.number().int().min(1).default(1) }); // chaîne de requête : conversion
z.iso.date(); z.email(); z.enum(['CR', 'CL']); z.number().positive().multipleOf(0.01);
z.stringbool(); // booléen dans l'URL
const r = schema.safeParse(donnees); // r.success, r.data, r.error.issues
Authentification
const hachage = await argon2.hash(motDePasse); // stockage
await argon2.verify(hachage, motDePasse); // connexion
jwt.sign({ role }, secret, { subject: String(id), expiresIn: 900, algorithm: 'HS256' });
jwt.verify(jeton, secret, { algorithms: ['HS256'] }); // jamais jwt.decode pour authentifier
| Contrôle | Où | Refus |
|---|---|---|
| Jeton valide | authentifier | 401 + WWW-Authenticate |
| Rôle | exigerRole('comptable') | 403 |
| Propriétaire | Service, après lecture en base | 404 |
Codes de statut de l'API
| Code | Quand |
|---|---|
200 | Lecture ou modification réussie (liste vide comprise) |
201 | Création, avec Location |
204 | Suppression, sans corps |
400 | JSON illisible |
401 | Pas de jeton, jeton invalide ou expiré, identifiants faux |
403 | Rôle insuffisant |
404 | Ressource inexistante ou d'un autre utilisateur ; route inconnue |
409 | Incompatible avec l'état actuel : doublon, transition interdite, fiche clôturée |
413, 415 | Corps ou fichier trop gros ; type de fichier refusé |
422 | Données invalides, avec la liste des champs |
429 | Trop de requêtes |
500 | Erreur imprévue : message générique et numéro d'incident |
Checklist avant une démonstration ou un rendu
-
npm cipuisnpm testpassent sur une copie propre du dépôt. - Le
.env.exampleliste toutes les variables ; aucun.envni secret dans Git. - Toutes les entrées passent par un schéma Zod ; aucun
req.bodytransmis tel quel. - Toutes les requêtes SQL sont préparées ; aucune concaténation de saisie.
- Chaque route protégée a
authentifier, le bonexigerRole, et le service vérifie le propriétaire. - Les mots de passe sont hachés avec argon2 (ou bcrypt) ; aucun secret dans les journaux.
- Les erreurs
500ne montrent ni pile, ni SQL ; elles donnent un numéro d'incident. - CORS en liste blanche, helmet branché, limitation de débit sur la connexion.
-
/docsà jour avec les routes réelles ; README avec les étapes de démarrage. - Même version de Node.js partout (
engines,Dockerfile, CI).