## Introduction

Un laboratoire local sûr permet d'essayer des idées, de déboguer des problèmes et d'apprendre rapidement sans risquer la production. Dans ce guide, vous allez :

- Monter une API Express minimale sur localhost

- Ajouter des réglages de sécurité par défaut (helmet, CORS, limitation de taux)

- Ajouter une journalisation structurée avec identifiants de requête

- Écrire quelques tests avec Jest + Supertest

- Exercer l'API avec curl et un test de charge basique

- Connecter optionnellement un MongoDB local

Le résultat est un flux de travail répétable et inspectable que vous pouvez étendre — par exemple, en ajoutant l'authentification, une file de messages ou des tâches en arrière-plan — au fur et à mesure que vos besoins grandissent.

## Aperçu du flux de travail

Suivez cette séquence et vérifiez après chaque étape.

### Préparer la chaîne d'outils

- Installez Node.js 18+ et Git. Optionnel : Docker pour un MongoDB local.

- Créez un répertoire frais pour le laboratoire :

mkdir express-lab && cd express-lab
npm init -y 

### Installer les dépendances

 npm install express helmet cors express-rate-limit pino pino-http zod dotenv
npm install --save-dev nodemon jest supertest cross-env 

### Structure du projet

 express-lab/
 .env # variables d'environnement (ne jamais commiter de secrets)
 package.json
 src/
 index.js # point d'entrée de l'application
 routes.js # routes d'exemple
 errors.js # helpers d'erreur
 db.js # connexion MongoDB optionnelle
 note.model.js # modèle Mongoose optionnel
 test/
 app.test.js # tests de base 

### Variables d'environnement

 Créez un fichier .env :

PORT=4000
HOST=127.0.0.1
CORS_ORIGIN=http://localhost:3000
RATE_LIMIT_WINDOW_MS=60000
RATE_LIMIT_MAX=60
NODE_ENV=development
LOG_LEVEL=info 

### Scripts dans package.json

 {
 "name": "express-lab",
 "version": "1.0.0",
 "type": "commonjs",
 "scripts": {
 "dev": "nodemon src/index.js",
 "start": "node src/index.js",
 "test": "cross-env NODE_ENV=test jest --runInBand --detectOpenHandles"
 },
 "jest": {
 "testEnvironment": "node",
 "verbose": false
 }
} 

### Code de l'application : sécurité, journalisation et points de terminaison

 src/errors.js

class HttpError extends Error {
 constructor(status, message) {
 super(message);
 this.status = status;
 }
}

function notFound(req, res, next) {
 next(new HttpError(404, 'Not found'));
}

function errorHandler(err, req, res, next) { // eslint-disable-line no-unused-vars
 const status = err.status || 500;
 res.status(status).json({
 error: {
 message: err.message || 'Internal server error',
 status
 }
 });
}

module.exports = { HttpError, notFound, errorHandler }; 
 src/routes.js

const express = require('express');
const { z } = require('zod');
const { HttpError } = require('./errors');

const router = express.Router();

router.get('/health', (req, res) => {
 res.json({ status: 'ok', uptime: process.uptime(), ts: Date.now() });
});

router.post('/echo', (req, res, next) => {
 const schema = z.object({ message: z.string().min(1) });
 const parsed = schema.safeParse(req.body);
 if (!parsed.success) return next(new HttpError(400, 'Invalid body'));
 res.json({ echoed: parsed.data.message });
});

router.get('/math/sum', (req, res, next) => {
 const raw = (req.query.numbers || '').toString();
 if (!raw) return next(new HttpError(400, 'Provide numbers query param'));
 const nums = raw.split(',').map(x => Number(x.trim()));
 if (nums.some(n => Number.isNaN(n))) return next(new HttpError(400, 'numbers must be comma-separated numerics'));
 const sum = nums.reduce((a, b) => a + b, 0);
 res.json({ sum, count: nums.length });
});

module.exports = router; 
 src/index.js

require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');
const rateLimit = require('express-rate-limit');
const pino = require('pino');
const pinoHttp = require('pino-http');
const routes = require('./routes');
const { notFound, errorHandler } = require('./errors');

const app = express();
const logger = pino({ level: process.env.LOG_LEVEL || 'info' });

app.disable('x-powered-by');
app.set('trust proxy', false);

app.use(pinoHttp({
 logger,
 genReqId: (req) => req.headers['x-request-id'] || `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
}));
app.use(helmet());
app.use(cors({ origin: process.env.CORS_ORIGIN || 'http://localhost:3000', credentials: true }));
app.use(express.json({ limit: '100kb' }));

const limiter = rateLimit({
 windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS || 60000),
 max: Number(process.env.RATE_LIMIT_MAX || 60),
 standardHeaders: true,
 legacyHeaders: false
});
app.use(limiter);

app.use('/api', routes);
app.use(notFound);
app.use(errorHandler);

const PORT = Number(process.env.PORT || 4000);
const HOST = process.env.HOST || '127.0.0.1';

app.listen(PORT, HOST, () => {
 logger.info({ PORT, HOST }, 'Server started');
}); 

### Lancer le laboratoire

 npm run dev
# Dans un autre terminal
curl -s http://127.0.0.1:4000/api/health | jq .
curl -s -X POST http://127.0.0.1:4000/api/echo -H 'Content-Type: application/json' -d '{"message":"hello"}' | jq .
curl -s 'http://127.0.0.1:4000/api/math/sum?numbers=1,2,3,4' | jq . 

### Tests de base

 test/app.test.js

const request = require('supertest');
const express = require('express');
const routes = require('../src/routes');
const { notFound, errorHandler } = require('../src/errors');

function createTestApp() {
 const app = express();
 app.use(express.json());
 app.use('/api', routes);
 app.use(notFound);
 app.use(errorHandler);
 return app;
}

describe('Express lab', () => {
 const app = createTestApp();

 test('GET /health returns ok', async () => {
 const res = await request(app).get('/api/health');
 expect(res.statusCode).toBe(200);
 expect(res.body.status).toBe('ok');
 });

 test('POST /echo requires message', async () => {
 const res = await request(app).post('/api/echo').send({});
 expect(res.statusCode).toBe(400);
 });

 test('GET /math/sum sums numbers', async () => {
 const res = await request(app).get('/api/math/sum').query({ numbers: '5,7,8' });
 expect(res.statusCode).toBe(200);
 expect(res.body.sum).toBe(20);
 });
}); 
 Exécutez les tests :

npm test 

### Vérifications rapides de charge et de sécurité

- Limitation de taux : déclenchez-la pour confirmer le comportement 429.

 for i in $(seq 1 70); do curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:4000/api/health; done | sort | uniq -c 
 Vous devriez voir 60 réponses avec 200 et 10 avec 429.

- Identifiant de requête : observez les logs qui incluent un identifiant de requête. Ajoutez -H 'x-request-id: demo-123' et confirmez que le même identifiant apparaît dans les logs.

curl -H 'x-request-id: demo-123' -s http://127.0.0.1:4000/api/health > /dev/null
# Vérifiez les logs du serveur pour demo-123 

### Optionnel : ajouter un MongoDB local

 Si vous souhaitez une couche de données réaliste, lancez MongoDB localement. Une option simple est d'exécuter un conteneur sur localhost.

# Démarrer MongoDB localement (réglages de développement par défaut)
docker run -d --name mongo-lab -p 27017:27017 mongo:6 
 Installez Mongoose et ajoutez une petite fonctionnalité de notes :

npm install mongoose 
 src/db.js

const mongoose = require('mongoose');

async function connect(uri) {
 mongoose.set('strictQuery', true);
 await mongoose.connect(uri);
}

module.exports = { connect }; 
 src/note.model.js

const mongoose = require('mongoose');

const NoteSchema = new mongoose.Schema({
 title: { type: String, required: true },
 body: { type: String, required: true }
}, { timestamps: true });

module.exports = mongoose.model('Note', NoteSchema); 
 Étendez src/routes.js avec des points de terminaison CRUD (restez minimal) :

// ajouter au début
const Note = require('./note.model');

// créer une note
router.post('/notes', async (req, res, next) => {
 try {
 const schema = z.object({ title: z.string().min(1), body: z.string().min(1) });
 const parsed = schema.safeParse(req.body);
 if (!parsed.success) return next(new HttpError(400, 'Invalid body'));
 const note = await Note.create(parsed.data);
 res.status(201).json({ id: note._id.toString() });
 } catch (e) { next(e); }
});

// lister les notes
router.get('/notes', async (req, res, next) => {
 try {
 const notes = await Note.find().sort({ createdAt: -1 }).limit(10).lean();
 res.json({ notes });
 } catch (e) { next(e); }
}); 
 Connectez la base de données dans src/index.js (ajoutez près du début) :

const { connect } = require('./db'); 
 Et remplacez le bloc app.listen par :

async function start() {
 const mongoUri = process.env.MONGO_URI || 'mongodb://127.0.0.1:27017/express_lab';
 if (process.env.NODE_ENV !== 'test') {
 await connect(mongoUri);
 }
 app.listen(PORT, HOST, () => {
 logger.info({ PORT, HOST }, 'Server started');
 });
}

start().catch(err => {
 logger.error({ err }, 'Failed to start');
 process.exit(1);
}); 
 Testez l'API des notes :

curl -s -X POST http://127.0.0.1:4000/api/notes \
 -H 'Content-Type: application/json' \
 -d '{"title":"first","body":"hello"}' | jq .

curl -s http://127.0.0.1:4000/api/notes | jq . 

### Astuces pour la reproductibilité

- Gardez un fichier .env.example dans le dépôt pour que les collègues connaissent les variables requises.

- Verrouillez la version de Node.js avec un .nvmrc ou l'outil de votre choix.

- Épinglez les versions majeures des dépendances pour éviter les ruptures surprises.

- Ajoutez un README court avec les exemples d'exécution, de test et de curl.

### Dépannage

 Symptôme Cause probable Solution 
 Port déjà utilisé Un autre processus sur le PORT Changez le PORT dans .env ou tuez l'autre processus 
 Erreurs CORS depuis un front-end sur un port différent CORS_ORIGIN mal configuré Définissez CORS_ORIGIN dans .env sur l'origine du front-end 
 Réponses 429 sur un trafic normal Limite de taux trop stricte Augmentez RATE_LIMIT_MAX ou RATE_LIMIT_WINDOW_MS 
 Connexion Mongo refusée Conteneur non démarré docker start mongo-lab et vérifiez docker ps 

## Plan pilote local

 Gardez le premier pilote étroit et vérifiable :

- Périmètre : /health , /echo , /math/sum uniquement ; middleware de sécurité ; journalisation ; 3 tests.

- Chronométrage : 60 minutes de zéro à tests passants.

- Critères de succès :

- GET /api/health renvoie status ok en < 50 ms localement

- POST /api/echo renvoie un message non vide

- GET /api/math/sum renvoie la somme correcte pour 1,2,3

- Les logs incluent un identifiant de requête, soit généré, soit provenant de l'en-tête x-request-id

- La limitation de taux renvoie 429 quand on dépasse le max défini (60 req/min par défaut)

- npm test passe en moins de 2 secondes sur un portable typique

- Objectif étendu : ajouter les points de terminaison /notes avec un MongoDB local et confirmer le CRUD de base.

## Conclusion

Vous disposez maintenant d'un laboratoire d'API Express compact, sécurisé et testable qui s'exécute entièrement sur localhost. La configuration est claire et répétable, vous pouvez donc expérimenter en toute sécurité, mesurer les changements et étendre les points de terminaison au fur et à mesure que vos besoins évoluent. Prochaines étapes :

- Étendre les tests pour couvrir les cas limites et les erreurs

- Introduire la validation de requête pour toutes les routes

- Ajouter une base de données locale ou une file de messages selon les besoins

- Emballer les scripts pour une configuration en une commande afin que toute l'équipe puisse exécuter le même laboratoire

Construisez incrémentalement, vérifiez chaque étape et gardez le pilote petit avant d'ajouter de la complexité.