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.exampledans le dépôt pour que les collègues connaissent les variables requises. - Verrouillez la version de Node.js avec un
.nvmrcou 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/sumuniquement ; middleware de sécurité ; journalisation ; 3 tests. - Chronométrage : 60 minutes de zéro à tests passants.
- Critères de succès :
- GET
/api/healthrenvoie status ok en < 50 ms localement - POST
/api/echorenvoie un message non vide - GET
/api/math/sumrenvoie la somme correcte pour1,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 testpasse en moins de 2 secondes sur un portable typique- Objectif étendu : ajouter les points de terminaison
/notesavec 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é.