Écrire une REST API avec Express dans Node.js
1. Aperçu
Cet article vous guide dans la construction d’une REST API de gestion des Order avec Express — du démarrage du projet, à la définition des routes, middleware, validation des données, jusqu’à la gestion centralisée des erreurs. Après avoir lu cet article, vous pouvez construire vous-même une API CRUD complète, avec des tests automatisés avec Jest et Supertest, et connaître les erreurs courantes quand on débute avec Express.
L’exemple tout au long de cet article : une API de gestion des commandes (Order) d’un Customer, supportant la création, la consultation, la modification et la suppression.
2. Préparation du projet
Créez un nouveau projet Node.js et installez Express :
mkdir order-api && cd order-api
npm init -y
npm install express
npm install --save-dev jest supertest
Ajoutez type: module pour utiliser la syntaxe import/export (ES Module), et déclarez le script de test dans package.json :
{
"name": "order-api",
"type": "module",
"scripts": {
"start": "node src/server.js",
"test": "node --experimental-vm-modules node_modules/.bin/jest"
}
}
Remarque : Jest avec ES Module nécessite le flag
--experimental-vm-modules. Si vous ne voulez pas de complications, vous pouvez supprimer"type": "module"et utiliserrequire/module.exports(CommonJS) — les deux sont valides, selon les préférences de votre équipe.
3. Créer un serveur Express basique
// src/app.js
import express from 'express';
const app = express();
app.use(express.json()); // parse le corps JSON en req.body
app.get('/health', (req, res) => {
res.json({ status: 'UP' });
});
export default app;
// src/server.js
import app from './app.js';
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Order API est en cours d'exécution sur le port ${PORT}`);
});
express.json()est un middleware intégré qui aide Express à parser automatiquementContent-Type: application/jsonen objetreq.body. Sans cette ligne,req.bodyseraundefined.- Séparer
app.jsdeserver.jspour que les tests n’aient pas besoin d’ouvrir un port réel — c’est une pratique importante pour les REST API Node.js.
Essayez :
node src/server.js
curl http://localhost:3000/health
{"status":"UP"}
4. Définir la ressource Order
Utilisez une “base de données” en mémoire avec un tableau pour vous concentrer sur la partie API, indépendamment d’une vraie BD.
// src/orders/orderRepository.js
let orders = [];
let nextId = 1;
export function findAll() {
return orders;
}
export function findById(id) {
return orders.find((o) => o.id === id);
}
export function create(data) {
const order = { id: nextId++, ...data, status: 'PENDING' };
orders.push(order);
return order;
}
export function update(id, data) {
const index = orders.findIndex((o) => o.id === id);
if (index === -1) return null;
orders[index] = { ...orders[index], ...data };
return orders[index];
}
export function remove(id) {
const index = orders.findIndex((o) => o.id === id);
if (index === -1) return false;
orders.splice(index, 1);
return true;
}
Ce repository simule les opérations CRUD que vous verrez dans n’importe quel ORM (Sequelize, Prisma…), la seule différence est qu’il stocke en RAM au lieu d’une BD.
5. Routage avec Express Router
Au lieu de coller toutes les routes à app, utilisez express.Router() pour modulariser par ressource :
// src/orders/orderRouter.js
import { Router } from 'express';
import * as repo from './orderRepository.js';
const router = Router();
router.get('/', (req, res) => {
res.json(repo.findAll());
});
router.get('/:id', (req, res, next) => {
const order = repo.findById(Number(req.params.id));
if (!order) return next({ status: 404, message: 'Order inexistant' });
res.json(order);
});
router.post('/', (req, res) => {
const order = repo.create(req.body);
res.status(201).json(order);
});
router.put('/:id', (req, res, next) => {
const order = repo.update(Number(req.params.id), req.body);
if (!order) return next({ status: 404, message: 'Order inexistant' });
res.json(order);
});
router.delete('/:id', (req, res, next) => {
const deleted = repo.remove(Number(req.params.id));
if (!deleted) return next({ status: 404, message: 'Order inexistant' });
res.status(204).end();
});
export default router;
Attachez le router à app avec le préfixe /orders :
// src/app.js (mise à jour)
import orderRouter from './orders/orderRouter.js';
app.use('/orders', orderRouter);
router.get('/:id', ...)—:idest un paramètre de route, accédé viareq.params.id(toujours une string, nécessiteNumber()pour comparer).- Appeler
next({ status: 404, message: ... })transfère l’erreur au middleware de gestion d’erreurs (section 7), au lieu d’écrireres.status(404)...répété à chaque route. 204 No Contentpour DELETE réussi — ne retourne pas de body.
6. Valider les données d’entrée
Valider manuellement avec if/else est facile d’oublier des cas. Utilisez express-validator pour être concis et clair :
npm install express-validator
// src/orders/orderValidator.js
import { body, validationResult } from 'express-validator';
export const validateCreateOrder = [
body('customerName').isString().notEmpty().withMessage('customerName est obligatoire'),
body('amount').isFloat({ gt: 0 }).withMessage('amount doit être supérieur à 0'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
next();
},
];
Attachez le validateur à la route POST :
// src/orders/orderRouter.js (mise à jour)
import { validateCreateOrder } from './orderValidator.js';
router.post('/', validateCreateOrder, (req, res) => {
const order = repo.create(req.body);
res.status(201).json(order);
});
- Chaque
body(...)est une règle qui vérifie un champ ; en cas d’erreur, les erreurs sont rassemblées dansvalidationResult(req). - Le dernier middleware dans le tableau vérifie
errors.isEmpty()— s’il y a des erreurs, retourne400avec des détails, ne l’appelle pasnext()pour arrêter la requête de continuer vers le vrai handler.
Envoyez une requête sans amount :
curl -X POST http://localhost:3000/orders \
-H "Content-Type: application/json" \
-d '{"customerName": "Nam"}'
{"errors":[{"type":"field","msg":"amount doit être supérieur à 0","path":"amount",...}]}
7. Gestion centralisée des erreurs
Express vous permet de définir un error-handling middleware — c’est un middleware avec 4 paramètres (err, req, res, next), placé après toutes les routes :
// src/errorHandler.js
export function errorHandler(err, req, res, next) {
const status = err.status || 500;
const message = err.message || 'Erreur système';
if (status === 500) {
console.error(err); // log l'erreur réelle pour déboguer, ne retournez pas la stack trace au client
}
res.status(status).json({ message });
}
// src/app.js (mise à jour, ajoutez en fin de fichier)
import { errorHandler } from './errorHandler.js';
app.use(errorHandler);
- Express reconnaît ce middleware comme error handler grâce au nombre de paramètres (4), pas le nom de la fonction.
- Vous devez placer
app.use(errorHandler)en dernier, après chaqueapp.use(router)— Express traite les middleware dans l’ordre déclaré. - Toute erreur
next(err)ou lancée dans une route (si vous utilisezexpress-async-errorsou try/catch) tombera ici.
Erreur courante : oublier d’appeler
next(err)dans du code async (par exempleawait db.query(...)lance) empêche Express de capturer l’erreur, le serveur plante ou la requête gèle. Avec Express 5 (les versions plus récentes), les gestionnaires async qui lancent des erreurs sont automatiquement capturés — avec Express 4, vous devez envelopper avec try/catch et appelernext(err)manuellement.
8. Ajouter une couche Service pour séparer la logique métier
Quand la logique devient plus complexe (par exemple : ne pas permettre de créer une commande si amount dépasse la limite du client), ne mettez pas tout dans le routeur. Séparez en orderService :
// src/orders/orderService.js
import * as repo from './orderRepository.js';
const MAX_AMOUNT = 100_000_000;
export function createOrder(data) {
if (data.amount > MAX_AMOUNT) {
const err = new Error('Dépassement de la limite autorisée');
err.status = 422;
throw err;
}
return repo.create(data);
}
// src/orders/orderRouter.js (mise à jour route POST)
import * as orderService from './orderService.js';
router.post('/', validateCreateOrder, (req, res, next) => {
try {
const order = orderService.createOrder(req.body);
res.status(201).json(order);
} catch (err) {
next(err);
}
});
Le routeur gère maintenant uniquement HTTP (code de statut, parse param), tandis que orderService gère la logique métier. C’est un modèle similaire à la couche Controller–Service dans d’autres frameworks, ce qui aide à tester la logique métier indépendamment du HTTP.
9. Écrire des tests avec Jest et Supertest
Supertest vous permet d’appeler l’API directement sur l’objet app (instance Express) sans avoir besoin d’ouvrir un vrai port — c’est pourquoi la section 3 sépare app.js de server.js.
// tests/order.test.js
import request from 'supertest';
import app from '../src/app.js';
describe('Order API', () => {
it('créer une commande avec succès retourne 201 et id', async () => {
const response = await request(app)
.post('/orders')
.send({ customerName: 'Nam', amount: 500000 });
expect(response.status).toBe(201);
expect(response.body).toMatchObject({
customerName: 'Nam',
amount: 500000,
status: 'PENDING',
});
expect(response.body.id).toBeDefined();
});
it('amount manquant retourne 400', async () => {
const response = await request(app)
.post('/orders')
.send({ customerName: 'Nam' });
expect(response.status).toBe(400);
});
it('récupérer une commande inexistante retourne 404', async () => {
const response = await request(app).get('/orders/9999');
expect(response.status).toBe(404);
expect(response.body.message).toBe('Order inexistant');
});
});
Exécutez les tests :
npm test
PASS tests/order.test.js
Order API
✓ créer une commande avec succès retourne 201 et id
✓ amount manquant retourne 400
✓ récupérer une commande inexistante retourne 404
request(app)initialise un client HTTP virtuel attaché directement à l’application Express — plus rapide que de tester sur le réseau réel.toMatchObjectvérifie uniquement les champs spécifiés, en ignorant les autres (commeidauto-généré) — pratique pour tester les réponses avec des valeurs dynamiques.
Remarque : puisque
ordersest un tableau en mémoire partagé entre les tests, les tests exécutés dans un certain ordre peuvent s’influencer mutuellement (par exemple, une commande créée dans le test 1 existe toujours quand le test 2 s’exécute). Avec une vraie API connectée à une BD, réinitialisez les données dansbeforeEachou utilisez un rollback de transaction.
10. Quand ne pas utiliser Express pur
- API grande, domaines complexes multiples : Express n’a pas de convention obligatoire sur la structure, il est facile de générer du “spaghetti code” si l’équipe ne définit pas ses propres conventions. Envisagez NestJS (avec DI, module, decorator comme Spring) si l’équipe est habituée à une architecture en couches bien définie.
- Besoin de type-safety fort : Express + JavaScript pur ne capture pas les erreurs de type au compile. Si le projet est grand, utilisez TypeScript dès le départ, ne attendez pas à “convertir plus tard” — le coût de conversion ultérieure est beaucoup plus élevé.
- Débit élevé, flux/WebSocket complexes : Express peut toujours fonctionner mais Fastify ou d’autres frameworks optimisés pour les performances (benchmark de sérialisation JSON plus rapide) peuvent être mieux adaptés aux systèmes à très fort trafic.
Pour les API de petite à moyenne taille, CRUD clair comme dans l’exemple ci-dessus, Express reste un choix rapide et simple, avec une grande communauté et des middleware tout-faits faciles à trouver (auth, rate-limit, CORS…).
11. Conclusion
À travers cet article, vous avez construit une REST API Order complète avec Express : le routage avec Router, la validation d’entrée avec express-validator, la gestion centralisée des erreurs avec un middleware à 4 paramètres, et la séparation de la logique métier dans une couche service pour faciliter les tests. Les tests avec Jest + Supertest vous permettent de refactoriser en confiance sans craindre de casser le comportement de l’API. À partir de cette base, vous pouvez ajouter une vraie connexion à une base de données (Prisma, Mongoose), l’authentification (JWT), ou passer à TypeScript à mesure que le projet grandit.