API REST avec Express
Qu'est-ce qu'une API REST ?
Une API REST (Representational State Transfer) est une interface qui permet à des applications de communiquer entre elles via HTTP. C'est le standard dominant du web : ton application mobile, ton front React ou Vue, et des services tiers consomment tous des API REST.
Express est l'outil parfait pour construire ce type d'API : léger, flexible, et basé sur les mécanismes HTTP que tu connais déjà.
1. Conventions REST : ressources, verbes, URLs
REST repose sur deux idées simples : les ressources (ce sur quoi on agit) et les verbes HTTP (l'action à effectuer).
Les URLs doivent nommer des ressources au pluriel et ne jamais contenir de verbe. On utilise le verbe HTTP pour exprimer l'intention.
// BONNE pratique — noms au pluriel, verbe HTTP = l'action
GET /todos // lister toutes les tâches
GET /todos/42 // lire la tâche #42
POST /todos // créer une nouvelle tâche
PUT /todos/42 // remplacer entièrement la tâche #42
PATCH /todos/42 // modifier partiellement la tâche #42
DELETE /todos/42 // supprimer la tâche #42
// MAUVAISE pratique — le verbe est dans l'URL
GET /getTodo/42 // ❌ redondant
POST /createTodo // ❌ inutile
GET /deleteTodo/42 // ❌ dangereux et non REST
2. CRUD complet avec Express
CRUD signifie Create, Read, Update, Delete — les quatre opérations fondamentales sur des données. Voici comment les implémenter avec Express en utilisant des données en mémoire.
const express = require('express');
const app = express();
// Middleware pour lire le JSON du body
app.use(express.json());
// Données en mémoire (un vrai projet utiliserait une base de données)
let todos = [
{ id: 1, titre: 'Apprendre Node.js', fait: true },
{ id: 2, titre: 'Construire une API', fait: false },
];
let nextId = 3;
// ─── READ — GET /todos ──────────────────────────────────────────────────────
app.get('/todos', (req, res) => {
res.json(todos); // 200 OK implicite
});
// ─── READ — GET /todos/:id ──────────────────────────────────────────────────
app.get('/todos/:id', (req, res) => {
const todo = todos.find(t => t.id === Number(req.params.id));
if (!todo) {
return res.status(404).json({ erreur: 'Tâche introuvable' });
}
res.json(todo);
});
// ─── CREATE — POST /todos ───────────────────────────────────────────────────
app.post('/todos', (req, res) => {
const { titre } = req.body;
if (!titre || titre.trim() === '') {
return res.status(400).json({ erreur: 'Le champ titre est requis' });
}
const nouveau = { id: nextId++, titre: titre.trim(), fait: false };
todos.push(nouveau);
res.status(201).json(nouveau); // 201 Created
});
// ─── UPDATE — PUT /todos/:id (remplacement complet) ─────────────────────────
app.put('/todos/:id', (req, res) => {
const index = todos.findIndex(t => t.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ erreur: 'Tâche introuvable' });
}
const { titre, fait } = req.body;
if (!titre) {
return res.status(400).json({ erreur: 'Le champ titre est requis' });
}
todos[index] = { id: todos[index].id, titre, fait: !!fait };
res.json(todos[index]);
});
// ─── UPDATE — PATCH /todos/:id (modification partielle) ─────────────────────
app.patch('/todos/:id', (req, res) => {
const todo = todos.find(t => t.id === Number(req.params.id));
if (!todo) {
return res.status(404).json({ erreur: 'Tâche introuvable' });
}
// On ne modifie que les champs fournis
if (req.body.titre !== undefined) todo.titre = req.body.titre;
if (req.body.fait !== undefined) todo.fait = req.body.fait;
res.json(todo);
});
// ─── DELETE — DELETE /todos/:id ─────────────────────────────────────────────
app.delete('/todos/:id', (req, res) => {
const index = todos.findIndex(t => t.id === Number(req.params.id));
if (index === -1) {
return res.status(404).json({ erreur: 'Tâche introuvable' });
}
todos.splice(index, 1);
res.status(204).send(); // 204 No Content — succès sans corps
});
app.listen(3000, () => console.log('API démarrée sur http://localhost:3000'));
3. Codes HTTP appropriés
Les codes de statut HTTP communiquent le résultat d'une requête. Les utiliser correctement est une marque de qualité d'une API.
// ── 2xx Succès ───────────────────────────────────────────────────────────────
200 OK // requête réussie (GET, PUT, PATCH)
201 Created // ressource créée avec succès (POST)
204 No Content // succès sans corps de réponse (DELETE)
// ── 4xx Erreurs client ───────────────────────────────────────────────────────
400 Bad Request // données invalides ou manquantes dans la requête
401 Unauthorized // authentification requise (pas de token, token expiré)
403 Forbidden // authentifié mais pas les droits suffisants
404 Not Found // ressource introuvable
// ── 5xx Erreurs serveur ──────────────────────────────────────────────────────
500 Internal // erreur inattendue côté serveur
Server Error
// Dans Express :
res.status(201).json(nouvelObjet); // avec corps JSON
res.status(204).send(); // sans corps
res.status(404).json({ erreur: '...' });// erreur avec message
401 vs 403 : 401 signifie "je ne sais pas qui tu es" (pas de token ou token invalide). 403 signifie "je sais qui tu es, mais tu n'as pas le droit". La nuance est importante pour les clients qui consomment l'API.
4. Validation du body
Valider les données entrantes est essentiel pour éviter les bugs et les données corrompues. On peut le faire manuellement ou avec une bibliothèque dédiée.
app.post('/users', (req, res) => {
const { nom, email, age } = req.body;
const erreurs = [];
if (!nom || typeof nom !== 'string' || nom.trim() === '') {
erreurs.push('Le nom est requis');
}
if (!email || !/.+@.+\..+/.test(email)) {
erreurs.push('Email invalide');
}
if (age !== undefined && (typeof age !== 'number' || age < 0)) {
erreurs.push('L\'âge doit être un nombre positif');
}
if (erreurs.length > 0) {
return res.status(400).json({ erreurs });
}
// Données valides — traitement...
res.status(201).json({ message: 'Utilisateur créé' });
});
// npm install zod
const { z } = require('zod');
// Définir le schéma de validation
const userSchema = z.object({
nom: z.string().min(1, 'Le nom est requis'),
email: z.string().email('Email invalide'),
age: z.number().min(0).optional(),
});
app.post('/users', (req, res) => {
const resultat = userSchema.safeParse(req.body);
if (!resultat.success) {
// Zod fournit des messages d'erreur précis par champ
return res.status(400).json({ erreurs: resultat.error.flatten().fieldErrors });
}
// resultat.data contient les données validées et typées
const { nom, email, age } = resultat.data;
res.status(201).json({ message: 'Utilisateur créé', nom });
});
Ne jamais faire confiance au client. Les données envoyées par un client peuvent être malformées, manquantes ou malveillantes. Valide toujours le body côté serveur, même si le front-end effectue déjà ses propres vérifications. La validation côté client est du confort UX, pas de la sécurité.
5. Structure de projet : routes, controllers, models
Mettre tout le code dans un seul fichier fonctionne pour les exemples, mais devient vite ingérable. La convention est de séparer les responsabilités en trois couches.
mon-api/
├── app.js // Point d'entrée — config Express, middlewares globaux
├── routes/
│ ├── todos.js // Définit les URLs et appelle les controllers
│ └── users.js
├── controllers/
│ ├── todosController.js // Logique métier : valide, traite, répond
│ └── usersController.js
├── models/
│ ├── todoModel.js // Accès aux données (BDD ou mémoire)
│ └── userModel.js
└── package.json
const express = require('express');
const router = express.Router();
const controller = require('../controllers/todosController');
router.get('/', controller.listerTous);
router.get('/:id', controller.lireUn);
router.post('/', controller.creer);
router.patch('/:id', controller.modifier);
router.delete('/:id', controller.supprimer);
module.exports = router;
const model = require('../models/todoModel');
exports.listerTous = (req, res) => {
res.json(model.getAll());
};
exports.lireUn = (req, res) => {
const todo = model.getById(Number(req.params.id));
if (!todo) return res.status(404).json({ erreur: 'Introuvable' });
res.json(todo);
};
exports.creer = (req, res) => {
const { titre } = req.body;
if (!titre) return res.status(400).json({ erreur: 'titre requis' });
const nouveau = model.create(titre);
res.status(201).json(nouveau);
};
exports.modifier = (req, res) => {
const todo = model.update(Number(req.params.id), req.body);
if (!todo) return res.status(404).json({ erreur: 'Introuvable' });
res.json(todo);
};
exports.supprimer = (req, res) => {
const ok = model.remove(Number(req.params.id));
if (!ok) return res.status(404).json({ erreur: 'Introuvable' });
res.status(204).send();
};
const express = require('express');
const app = express();
const todosRouter = require('./routes/todos');
app.use(express.json());
app.use('/todos', todosRouter); // préfixe /todos pour toutes les routes du router
app.listen(3000);
6. Tester son API avec curl et Postman
Avant de brancher un front-end, il faut tester l'API directement. Deux outils incontournables : curl (en ligne de commande) et Postman ou Insomnia (interface graphique).
# GET — lister toutes les tâches
curl http://localhost:3000/todos
# GET — lire une tâche spécifique
curl http://localhost:3000/todos/1
# POST — créer une tâche
# -X POST : méthode HTTP
# -H : header (Content-Type en JSON)
# -d : données du body
curl -X POST http://localhost:3000/todos \
-H "Content-Type: application/json" \
-d '{"titre": "Lire la doc Express"}'
# PATCH — marquer une tâche comme faite
curl -X PATCH http://localhost:3000/todos/1 \
-H "Content-Type: application/json" \
-d '{"fait": true}'
# DELETE — supprimer une tâche
curl -X DELETE http://localhost:3000/todos/2
# Afficher le code HTTP de la réponse (-I pour les headers)
curl -I -X DELETE http://localhost:3000/todos/99
Postman et Insomnia offrent une interface visuelle pour construire tes requêtes, sauvegarder des collections de tests et inspecter les réponses en détail. Idéal pour les équipes et les API complexes.
7. Exemple complet : API de gestion de tâches
Voici l'ensemble du projet en un seul fichier pour démarrer rapidement, puis la version structurée qui s'applique en production.
// Données en mémoire — remplace par une vraie BDD (SQLite, PostgreSQL...)
let todos = [{ id: 1, titre: 'Premier todo', fait: false }];
let nextId = 2;
module.exports = {
getAll: () => todos,
getById: (id) => todos.find(t => t.id === id),
create: (titre) => {
const t = { id: nextId++, titre, fait: false };
todos.push(t);
return t;
},
update: (id, champs) => {
const t = todos.find(t => t.id === id);
if (!t) return null;
if (champs.titre !== undefined) t.titre = champs.titre;
if (champs.fait !== undefined) t.fait = champs.fait;
return t;
},
remove: (id) => {
const index = todos.findIndex(t => t.id === id);
if (index === -1) return false;
todos.splice(index, 1);
return true;
},
};
- Les URLs REST nomment des ressources au pluriel — le verbe HTTP exprime l'action (
GET,POST,PUT,PATCH,DELETE) POSTretourne 201 Created,DELETEretourne 204 No Content, le reste retourne 200 OK400= données invalides ·401= non authentifié ·403= non autorisé ·404= introuvable- Toujours valider le body côté serveur — manuellement ou avec Zod / Joi
- Sépare les responsabilités :
routes/(URLs) →controllers/(logique) →models/(données) - Teste avec
curlen ligne de commande ou Postman/Insomnia pour une interface graphique express.json()est indispensable pour lire le body JSON des requêtesPOSTetPATCH