Une API Express est l'un des moyens les plus rapides de construire un backend avec Node.js : quelques lignes suffisent pour lancer un serveur HTTP, répondre aux requêtes et se découper proprement en couches à mesure qu'il grandit. Dans cet article, nous allons construire une petite mais véritable API REST à partir de zéro — organiser les routes avec des routeurs, regrouper le travail commun dans des middlewares, et terminer par une gestion centralisée des erreurs qui vous libère des blocs try/catch éparpillés.
Initialiser le projet
Commencez par un dossier vide et installez Node et Express. Node.js 18 ou une version ultérieure est recommandé ; ces versions prennent en charge fetch et les fonctionnalités JavaScript modernes de manière native.
mkdir blog-api && cd blog-api
npm init -y
npm install express
npm install --save-dev nodemon
Ajoutez "type": "module" à votre package.json pour activer les modules ES afin d'utiliser la syntaxe import. Définissons aussi un script qui redémarre le serveur quand un fichier change pendant le développement :
{
"type": "module",
"scripts": {
"dev": "nodemon server.js",
"start": "node server.js"
}
}
Le premier serveur
Le cœur d'une application Express est un objet app. Nous ajoutons le middleware express.json() pour analyser les corps JSON entrants et commençons par une simple route de contrôle de santé.
// server.js
import express from "express";
const app = express();
app.use(express.json());
app.get("/health", (req, res) => {
res.json({ status: "ok", uptime: process.uptime() });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`API en marche : http://localhost:${PORT}`);
});
Lancez npm run dev et visitez http://localhost:3000/health — vous devriez voir une réponse JSON. C'est tout ; vous avez une API. Préparons-la maintenant à grandir.
Séparer les routes avec un Router
Entasser chaque route dans server.js fonctionne pour les tout petits projets mais devient vite illisible. L'objet Router d'Express vous permet de regrouper les routes liées dans un fichier séparé et de les monter sous un seul préfixe. Supposons que nous gérions une ressource d'articles (posts).
// routes/posts.js
import { Router } from "express";
const router = Router();
const posts = [
{ id: 1, title: "Bonjour le monde", body: "Premier article" }
];
router.get("/", (req, res) => {
res.json(posts);
});
router.get("/:id", (req, res) => {
const post = posts.find(p => p.id === Number(req.params.id));
if (!post) {
return res.status(404).json({ error: "Article introuvable" });
}
res.json(post);
});
router.post("/", (req, res) => {
const { title, body } = req.body;
const post = { id: posts.length + 1, title, body };
posts.push(post);
res.status(201).json(post);
});
export default router;
Montez ensuite ce routeur dans l'application principale sous le préfixe /posts :
import postsRouter from "./routes/posts.js";
app.use("/posts", postsRouter);
Désormais GET /posts, GET /posts/1 et POST /posts fonctionnent tous. Chaque nouvelle ressource (utilisateurs, commentaires) reçoit son propre fichier de routeur, et server.js reste épuré.
Middleware : regrouper le travail commun en un seul endroit
Un middleware est une chaîne de fonctions par laquelle une requête passe avant d'atteindre une réponse. Sa signature est (req, res, next) ; lorsqu'il a terminé, il appelle next() pour passer la main au suivant. La journalisation, l'authentification, la limitation de débit et autres préoccupations transversales vivent ici. Écrivons un simple enregistreur de requêtes :
// middleware/logger.js
export function logger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
const ms = Date.now() - start;
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} - ${ms}ms`);
});
next();
}
Ajoutez-le avant toutes les routes et chaque requête est journalisée automatiquement :
import { logger } from "./middleware/logger.js";
app.use(logger);
La validation peut aussi être un middleware. Par exemple, une petite garde qui vérifie le corps avant POST /posts :
export function validatePost(req, res, next) {
const { title } = req.body;
if (!title || title.trim() === "") {
return res.status(400).json({ error: "le titre est obligatoire" });
}
next();
}
Attachez-le ensuite uniquement à cette route : router.post("/", validatePost, handler). C'est là toute la puissance des middlewares — écrire une logique répétée une fois et la brancher partout.
Gestion centralisée des erreurs
L'une des fonctionnalités les plus utiles d'Express est un middleware d'erreur spécial à quatre arguments : (err, req, res, next). Cette fonction est définie après toutes les routes et capture toute erreur levée n'importe où. Ainsi vous n'avez pas à parsemer un try/catch distinct dans chaque handler.
Pour que les erreurs des fonctions asynchrones l'atteignent, l'approche la plus propre est un petit enrobage (Express 5 transmet automatiquement les erreurs asynchrones, mais en 4.x ce helper est pratique) :
// utils/asyncHandler.js
export const asyncHandler = (fn) => (req, res, next) =>
Promise.resolve(fn(req, res, next)).catch(next);
Nous pouvons définir une classe d'erreur et la lever confortablement dans asyncHandler :
// utils/ApiError.js
export class ApiError extends Error {
constructor(status, message) {
super(message);
this.status = status;
}
}
Enfin, nous ajoutons le middleware d'erreur et un attrape-tout 404 pour les routes inconnues. Ceux-ci doivent toujours venir en dernier :
// 404 — aucune route ne correspond
app.use((req, res) => {
res.status(404).json({ error: "Ressource introuvable" });
});
// Gestion centralisée des erreurs
app.use((err, req, res, next) => {
const status = err.status || 500;
if (status === 500) console.error(err);
res.status(status).json({ error: err.message || "Erreur serveur" });
});
Désormais un simple throw new ApiError(404, "Article introuvable") dans un handler suffit ; ce point unique gère le reste. En production, il est de bonne pratique de garder le message générique pour les 500, comme ci-dessus, afin de ne pas divulguer de détails au client.
Garder la structure propre
À mesure que le projet grandit, une organisation simple des dossiers le rend maintenable :
routes/— un fichier de routeur par ressource.controllers/— la véritable logique métier qui traite une route (garde le routeur léger).middleware/— des pièces réutilisables comme logger, auth, validation.utils/— des helpers tels queasyncHandleretApiError.
Avec cette séparation, le routeur répond seulement à « quelle URL va vers quelle fonction », et le vrai travail vit dans le contrôleur. Quand vous ajoutez une base de données (PostgreSQL ou MongoDB par exemple), cette structure évolue presque sans changement.
Questions fréquentes
Ne pourrais-je pas simplement utiliser le module http intégré au lieu d'Express ?
Vous le pourriez, mais il faudrait écrire le routage, l'analyse du corps et la chaîne de middlewares à la main. Express fournit tout cela sous forme d'une couche légère ; il est facile à apprendre et son écosystème est immense. Même pour une petite API, il fait gagner du temps.
Pourquoi l'ordre des middlewares est-il important ?
Express exécute les middlewares dans l'ordre où ils sont ajoutés avec app.use. Vous ne pouvez pas valider un corps avant que express.json() ne l'analyse, et le middleware d'erreur doit venir après toutes les routes pour pouvoir capturer leurs erreurs.
Dois-je utiliser Express 4 ou 5 ?
Express 5 est désormais stable et transmet automatiquement les erreurs des handlers asynchrones au middleware d'erreur. Pour les nouveaux projets, vous pouvez préférer la 5 ; sur les projets 4.x existants, le motif asyncHandler ci-dessus est une solution sûre et répandue.
Vous voulez faire passer votre API au niveau supérieur ? Si vous avez besoin d'authentification, d'intégration de base de données ou d'une fondation Express prête pour la production, nous pouvons l'examiner ensemble. Contactez-moi.