Cette fois j'ai juste besoin d’une API qui fait le job sans se prendre la tête avec des frameworks surdimensionnés ou des architectures dignes d’un projet de la NASA, voici comment créer une petite API REST avec Node.js pur. Quelques routes CRUD, un stockage en mémoire simpliste, et zéro dépendance superflue. Le tout dans un seul petit fichier JavaScript. Oui, c’est possible. Non, ce n’est pas une blague.
Pourquoi Node.js pur ?
Parce que les frameworks, c’est comme les régimes : ça promet monts et merveilles, mais au final, on se retrouve avec plus de complexité que de résultats.
Express, Fastify, NestJS… Tous ces frameworks sont géniaux, mais ils viennent avec leur lot de conventions, de middlewares et de configurations qui, soyons honnêtes, sont souvent overkill pour un petit projet. Si vous voulez juste exposer quelques routes pour gérer des données, Node.js pur suffit amplement. Et puis, avouons-le, coder sans framework, c’est un peu comme conduire une voiture sans assistance : ça demande un peu plus d’effort, mais on se sent plus libre.
Pour cet exemple, on va utiliser le module http intégré à Node.js et un stockage en mémoire basé sur un simple tableau JavaScript. Pas de SQL, pas de MongoDB, pas de Redis. Juste un tableau. Parce que parfois, la simplicité est la meilleure des solutions.
Le code : quelques routes CRUD, zéro prise de tête
Voici le code complet de notre API. On va créer un serveur HTTP avec cinq couples route/méthode utilisant GET, POST, PUT et DELETE. Notre stockage sera simplement un tableau d’objets en mémoire, et on va gérer les requêtes manuellement. C’est basique, mais ça marche.
const http = require('http');
// Notre stockage en mémoire simpliste
let items = [
{ id: 1, name: 'Premier item' },
{ id: 2, name: 'Deuxième item' }
];
// Fonction pour générer un ID unique
const generateId = () => {
return items.length > 0 ? Math.max(...items.map(item => item.id)) + 1 : 1;
};
// Création du serveur
const server = http.createServer((req, res) => {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
// Endpoint GET /items
if (req.method === 'GET' && req.url === '/items') {
res.writeHead(200);
res.end(JSON.stringify(items));
}
// Endpoint GET /items/:id
else if (req.method === 'GET' && /^\/items\/\d+$/.test(req.url)) {
const id = Number(req.url.split('/')[2]);
const item = items.find(item => item.id === id);
if (item) {
res.writeHead(200);
res.end(JSON.stringify(item));
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Item not found' }));
}
}
// Endpoint POST /items
else if (req.method === 'POST' && req.url === '/items') {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const newItem = JSON.parse(body);
newItem.id = generateId();
items.push(newItem);
res.writeHead(201);
res.end(JSON.stringify(newItem));
} catch (error) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
}
// Endpoint PUT /items/:id
else if (req.method === 'PUT' && /^\/items\/\d+$/.test(req.url)) {
const id = Number(req.url.split('/')[2]);
const itemIndex = items.findIndex(item => item.id === id);
if (itemIndex !== -1) {
let body = '';
req.on('data', chunk => {
body += chunk.toString();
});
req.on('end', () => {
try {
const updatedItem = JSON.parse(body);
updatedItem.id = id;
items[itemIndex] = updatedItem;
res.writeHead(200);
res.end(JSON.stringify(updatedItem));
} catch (error) {
res.writeHead(400);
res.end(JSON.stringify({ error: 'Invalid JSON' }));
}
});
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Item not found' }));
}
}
// Endpoint DELETE /items/:id
else if (req.method === 'DELETE' && /^\/items\/\d+$/.test(req.url)) {
const id = Number(req.url.split('/')[2]);
const itemIndex = items.findIndex(item => item.id === id);
if (itemIndex !== -1) {
items.splice(itemIndex, 1);
res.writeHead(204);
res.end();
} else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Item not found' }));
}
}
// Endpoint non trouvé
else {
res.writeHead(404);
res.end(JSON.stringify({ error: 'Endpoint not found' }));
}
});
// Démarrage du serveur
const PORT = 3000;
server.listen(PORT, () => {
console.log(`Serveur démarré sur http://localhost:${PORT}`);
});
Ce code est volontairement minimaliste. Pas de validation poussée des données, pas de gestion des erreurs ultra-détaillée, pas de middlewares. Juste l’essentiel pour faire fonctionner une API CRUD. Si vous voulez ajouter des fonctionnalités, libre à vous de le faire, mais gardez en tête que chaque ligne de code supplémentaire est une ligne de code à maintenir.
Les données sont stockées uniquement en mémoire : elles disparaissent donc à chaque redémarrage du serveur. C’est parfaitement acceptable pour un exemple ou un prototype, beaucoup moins pour votre future API bancaire.
Les limites : oui, c’est simpliste, et alors ?
Évidemment, cette approche a ses limites. Un simple tableau JavaScript en mémoire, c’est sympa pour un prototype ou un petit projet, mais ça ne tiendra pas la route en production. Les données ne sont pas persistées, il n’y a aucune validation de schéma, aucune authentification et aucune limitation de la taille des requêtes.
Dans les routes POST et PUT, par exemple, le corps de la requête est accumulé directement en mémoire sans limite particulière. Pour un exemple pédagogique, aucun problème. Pour une API exposée directement sur Internet, ce serait une autre histoire.
De même, gérer manuellement les requêtes HTTP est parfaitement faisable, mais dès que l’API commence à grossir, on finit rapidement par réinventer une bonne partie de ce qu’Express ou Fastify font déjà très bien.
Cependant, pour un petit projet perso, un MVP ou simplement pour comprendre comment fonctionnent les APIs REST sous le capot, cette solution est excellente. Elle permet de se concentrer sur l’essentiel sans se perdre dans des abstractions ou des couches techniques dont on n’a pas encore besoin.
Et puis, avouons-le, c’est toujours satisfaisant de coder quelque chose qui fonctionne avec aussi peu de code.
Et si l’API commence à grossir ?
Si vous voulez aller plus loin, vous pouvez remplacer le tableau par une vraie base de données comme SQLite, PostgreSQL ou MongoDB, ajouter de la validation, gérer les logs, limiter la taille des requêtes ou ajouter une authentification.
Vous pouvez aussi basculer vers un framework comme Express ou Fastify lorsque les routes commencent à se multiplier. Le but n’est pas de dire que les frameworks sont inutiles. Ils deviennent simplement beaucoup plus intéressants lorsqu’on commence réellement à avoir les problèmes qu’ils sont censés résoudre.
Conclusion : la simplicité, c’est la clé
En développement, on a souvent tendance à vouloir tout complexifier. On ajoute des frameworks, des bibliothèques, des outils, des configurations… jusqu’à ce qu’on ne comprenne plus vraiment ce qu’on est en train de coder.
Parfois, il suffit de revenir à l’essentiel pour réaliser qu’un serveur HTTP, quelques conditions et un tableau JavaScript permettent déjà de comprendre une grosse partie du fonctionnement d’une API REST.
Cette API minimaliste avec Node.js pur est un bon exemple de ce principe. Quelques routes CRUD, un stockage en mémoire et aucune dépendance externe. C’est largement suffisant pour apprendre, tester une idée ou construire rapidement un prototype.
Et si un jour le projet devient plus gros, il sera toujours temps de sortir Express, Fastify ou autre chose. Mais autant attendre d’avoir un problème avant d’installer la solution.