Node.js : de la requête simple à une API plus souple, jointures, paramètres et affichage dynamique
Dans l’article précédent, « Connecter Node.js à une base MySQL pour fournir des données JSON », nous avons posé les bases : établir une connexion, exécuter une requête simple et renvoyer des données exploitables côté navigateur. Ce point d’entrée volontairement direct nous a permis de comprendre le chemin complet, de la requête HTTP jusqu’au rendu.
Nous allons ici prolonger cette approche, non pas en la complexifiant, mais en l’affinant. L’objectif est d’ouvrir progressivement vers des usages plus souples : enrichir les données dès la requête SQL, introduire des paramètres dynamiques, structurer le serveur, et amorcer une logique d’API plus évolutive. Les fichiers nécessaires à cet article seront téléchargeables via le lien « Node-Server-MySQL-Part-II ».
Note de contexte et compatibilité
Il est important de noter que cet article s’inscrit dans une continuité de travail initiée en 2021. À ce titre, certains exemples reposent sur l’utilisation de url.parse(), une méthode aujourd’hui en voie de dépréciation dans Node.js. Ces exemples restent néanmoins pleinement fonctionnels et permettent de comprendre les mécanismes fondamentaux mis en place : lecture des paramètres, routage, structuration des échanges. Ils sont donc volontairement conservés dans leur forme initiale afin de préserver la cohérence pédagogique de la progression.
Pour accompagner cette évolution, un chapitre dédié, « Évolution de l’API URL : remplacer url.parse() », présente les adaptations désormais recommandées avec l’API URL. Il permet de faire le lien entre pratiques historiques et standards actuels, sans remettre en cause les bases construites dans cet article.
Repartir sur une base connue
Avant d’aborder la première exploration, prenons un instant pour rappeler l’architecture mise en place dans l’article précédent. Nous repartons exactement du même socle, ce qui permet de se concentrer ici sur les évolutions sans changer de cadre :
project
│
├── server.js
├── db.js
│
├── api
│ └── albums.js
│
├── public
│ ├── index.html
│ ├── script.js
│
└── package.jsonL’ensemble reste volontairement simple : un serveur Node.js, une connexion MySQL, un point d’entrée API, et une interface minimale côté navigateur. C’est cette continuité qui va nous permettre d’affiner progressivement le fonctionnement.
Pour repartir proprement, il suffit de vérifier quelques points côté environnement. Depuis la console, on s’assure que le serveur éventuellement encore en cours d’exécution est bien arrêté, Ctrl + C. On se positionne ensuite dans le nouveau dossier de travail, puis on lance si nécessaire un npm install afin de réinstaller les dépendances, ce qui entraînera la création du dossier node_modules à la racine du projet.
Une fois ces éléments en place, le serveur peut être relancé, via npm start ou node server.js selon la configuration définie dans le fichier package.json. Nous retrouvons alors exactement la situation laissée à la fin de l’article précédent, prête à être enrichie.
Mettre en place une table de routage
À ce stade, tel que nous l’avions laissé à la fin du chapitre précédent, chaque nouvelle route API impliquait encore d’ajouter un test directement dans server.js. Cela fonctionne, mais devient rapidement difficile à maintenir dès que les endpoints se multiplient. Pour gagner en souplesse, on peut introduire une table de routage. Avant de l’utiliser, précisons ce qu’est l’objet routes. Il s’agit simplement d’un objet JavaScript qui va nous permettre d’associer une URL à une fonction. La clé représente l’URL appelée par le navigateur, et la valeur correspond à la fonction chargée de traiter la requête. Par exemple :
const routes = {
'/api/albums': albumsRoute
};Dans cet objet :
'/api/albums'est l’URL demandée par le navigateur ;albumsRouteest la fonction (importée depuisapi/albums.js) qui va exécuter la requête SQL et renvoyer la réponse.
Le principe devient alors très simple : le serveur regarde si l’URL demandée existe dans routes, et si c’est le cas, il appelle directement la fonction associée en lui transmettant req et res. Chaque endpoint correspond donc à un fichier dans le dossier api, à une fonction importée dans server.js, puis à une entrée dans cet objet routes. Ajouter une nouvelle fonctionnalité revient alors à trois actions très claires : créer un fichier, l’importer, puis l’ajouter dans la table. On peut ainsi éviter de multiplier les tests du type if (req.url === ...) et centraliser le routage dans une structure unique, plus lisible et plus évolutive.
// server_II_01.js // --- Imports des routes API ---
import albumsRoute from './api/albums.js';
// import bandsRoute from './api/bands.js'; // (exemple d’un futur endpoint)
// --- Table de routage ---
// Associe chaque URL à la fonction qui doit traiter la requête
const routes = {
'/api/albums': albumsRoute,
// Pour ajouter un nouvel endpoint :
// 1. créer le fichier api/bands.js
// 2. importer la fonction (voir ligne ci-dessus)
// 3. ajouter une entrée ici :
// '/api/bands': bandsRoute
};
// --- Point d’entrée du serveur ---
const server = http.createServer((req, res) => {
// Si l’URL correspond à une route API connue
if (routes[req.url]) {
return routes[req.url](req, res);
}
// ... le reste du serveur (fichiers statiques, etc.)
});Cette approche reste très simple, mais elle permet déjà de garder un serveur lisible même lorsque plusieurs routes API sont ajoutées. Il suffit d’ajouter une nouvelle entrée dans l’objet routes pour connecter une nouvelle fonctionnalité du dossier api. Ce type d’organisation prépare naturellement l’utilisation de frameworks côté serveur comme Express.js, qui prennent en charge ce mécanisme de routage. Côté interface, des outils comme Vue.js s’inscrivent dans cette continuité : les appels à l’API viennent alimenter des composants, sans changer la manière de penser les échanges entre données et affichage.

Enrichir les données dès la requête SQL
Nous avions volontairement utilisé une requête SQL très simple, afin de nous concentrer sur le mécanisme global. Mais en pratique, SQL permet d’aller beaucoup plus loin : jointures, agrégations, filtres, tris… tout peut être préparé directement côté base. L’idée n’est pas tant de complexifier la requête pour l’exemple, mais de comprendre que la base de données peut déjà structurer une grande partie des données attendues par l’interface. Plutôt que de multiplier les traitements côté JavaScript, on peut rapprocher les informations dès la source.
SELECT ALBUMS.*, BANDS.ch_band_label
FROM tab_albums AS ALBUMS
JOIN tab_bands AS BANDS ON ALBUMS.ch_album_band_id = BANDS.ch_band_idCette jointure n’est ici qu’une illustration parmi d’autres. Elle montre surtout que l’on peut préparer des données prêtes à l’emploi avant qu’elles n’arrivent dans l’application. Pour aller un peu plus loin dans ce sens, on peut également clarifier le code côté serveur en isolant la requête SQL dans une variable. Cela rend l’écriture plus lisible et facilite son évolution.
// albums_II_02.js
import { db } from '../db.js';
const sql = `
SELECT ALBUMS.*, BANDS.ch_band_label
FROM tab_albums AS ALBUMS
JOIN tab_bands AS BANDS ON ALBUMS.ch_album_band_id = BANDS.ch_band_id
`;
export default function albumsRoute(req, res) {
db.query(sql, (err, results) => {
if (err) {
res.writeHead(500);
res.end('Erreur serveur');
return;
}
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify(results));
});
}Ce découpage peut sembler anodin, mais il améliore immédiatement la lecture : la requête est identifiable en un coup d’œil, et la fonction se concentre sur son rôle, recevoir une requête, renvoyer une réponse. Après modification de ce fichier côté serveur, il est nécessaire de redémarrer Node.js (Ctrl + C puis relance), car le code est chargé en mémoire au démarrage et n’est pas mis à jour automatiquement.
Côté interface, cette évolution a aussi un impact direct. Puisque nous avons enrichi les données avec le nom du groupe (ch_band_label), il devient naturel de l’exploiter dans l’affichage.
// script_II_02.js
// seule la boucle data.forEach est à adapter ici afin d’intégrer les nouvelles données du groupe (ch_band_label)
data.forEach(album => {
const li = document.createElement('li');
const lien = document.createElement('a');
lien.href = album.ch_album_link;
lien.textContent = album.ch_album_label;
lien.target = '_blank';
const band = document.createElement('span');
band.textContent = ` (${album.ch_band_label})`;
li.appendChild(lien);
li.appendChild(band);
liste.appendChild(li);
});On voit ici apparaître un enchaînement simple mais structurant : une donnée enrichie côté SQL devient immédiatement exploitable côté interface, sans transformation supplémentaire. C’est précisément ce lien direct entre requête et affichage qui rend l’ensemble plus fluide à faire évoluer.

Ne plus figer le filtrage
Jusqu’ici, les requêtes SQL ont été écrites « en dur », avec des valeurs directement intégrées dans la clause WHERE. Cela fonctionne, mais limite rapidement les évolutions. Dès que l’on souhaite faire varier le résultat sans réécrire la requête, il devient nécessaire de la rendre paramétrable. On introduit alors le binding de paramètres. Le principe est simple : remplacer une valeur fixe par un placeholder (?), puis fournir la valeur au moment de l’exécution.
// albums_II_03.js
const sql = `
SELECT ALBUMS.*, BANDS.ch_band_label
FROM tab_albums AS ALBUMS
JOIN tab_bands AS BANDS ON ALBUMS.ch_album_band_id = BANDS.ch_band_id
WHERE ALBUMS.ch_album_etat = ?
`;
const etat = 1;
db.query(sql, [etat], (err, results) => {
// ici la suite du code (gestion de l'erreur, réponse JSON...)
});Le ? représente un emplacement réservé. Le tableau [etat] contient les valeurs à injecter. Chaque élément du tableau remplace un ?, dans l’ordre. On ne concatène plus une chaîne SQL, on décrit une requête, puis on lui fournit des données. Cette séparation rend le code plus lisible et plus sûr.

Transmettre les paramètres avec la méthode GET
La méthode GET paraît simple à mettre en place, mais dans notre architecture actuelle il faut tenir compte d’un point important : dès que nous ajoutons des paramètres à l’URL, req.url ne vaut plus seulement /api/albums, mais /api/albums?etat=1&band=4&year=1972. Or, dans server.js, notre routage repose sur un objet routes indexé par chemin :
const routes = {
'/api/albums': albumsRoute,
};
const server = http.createServer((req, res) => {
if (routes[req.url]) {
return routes[req.url](req, res);
}
});Dans ce cas, la présence de ?etat=1... empêche la correspondance directe, puisque la clé recherchée n’est plus exactement '/api/albums'. C’est donc à ce niveau qu’il faut agir. L’idée la plus simple consiste à séparer le chemin et les paramètres dès l’entrée dans le serveur. On utilise pour cela url.parse() dans server.js, afin de router sur le seul pathname, puis de laisser la route récupérer les paramètres si besoin. Une fois cette adaptation en place, l’appel suivant fonctionne correctement :
// server_II_04.js
import http from 'http';
import url from 'url';
import albumsRoute from './api/albums.js';
const routes = {
'/api/albums': albumsRoute,
};
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true); // ex: { pathname: '/api/albums', query: { etat: '1', band: '4', year: '1972' }, ... }
const pathname = parsedUrl.pathname; // ex: '/api/albums' (sans les paramètres)
// même principe pour un fichier statique :
// '/index.html?etat=1...' devient '/index.html'
// Les paramètres d’URL ne font pas partie du chemin du fichier : ils sont ignorés pour la lecture du fichier,
// mais restent disponibles pour un traitement applicatif via parsedUrl.query si nécessaire.
if (routes[pathname]) {
return routes[pathname](req, res);
}
const target = pathname === '/' ? 'index.html' : pathname;
const filePath = path.join(root, target);
fs.readFile(filePath, (err, content) => {
// code identique
});
});Important : le serveur ne reçoit que les paramètres présents dans l’URL appelée (celle du
fetch). Les paramètres de la page hôte ne sont pas transmis automatiquement. Il faut donc les inclure explicitement dans l’URL de l’API côté front.
Pour que le serveur reçoive effectivement ces paramètres, il faut les lire dans l’URL de la page (celle affichée dans le navigateur), puis les réinjecter explicitement dans l’appel à l’API. Autrement dit, on passe d’une URL “page” à une URL “API” en reconstruisant une query string cohérente. Le code suivant illustre cette étape de manière concrète.
// script_II_04.js
const params = new URLSearchParams(window.location.search);
const etat = params.get('etat') ?? 1;
const band = params.get('band') ?? '';
const year = params.get('year') ?? '';
const query = new URLSearchParams({ etat, band, year }).toString();
fetch(`/api/albums?${query}`);Ainsi, les paramètres de la page sont bien transmis au serveur via l’appel API.
/api/albums?etat=1&band=4&year=1972Dans la route elle même, une fois le routage effectué sur le pathname, on récupère explicitement les paramètres transmis dans l’URL de la requête. Ceux ci se trouvent dans la partie query issue de url.parse(req.url, true) et correspondent exactement aux valeurs envoyées par le navigateur via fetch. On peut alors les lire, leur appliquer des valeurs par défaut si nécessaire, puis les injecter dans la requête SQL via le mécanisme de binding :
// albums_II_04.js
import { db } from '../db.js';
import url from 'url';
const sql = `
SELECT ALBUMS.*, BANDS.ch_band_label
FROM tab_albums AS ALBUMS
JOIN tab_bands AS BANDS ON ALBUMS.ch_album_band_id = BANDS.ch_band_id
WHERE ALBUMS.ch_album_etat = ?
OR ALBUMS.ch_album_band_id = ?
OR ALBUMS.ch_album_year = ?
`;
export default function albumsRoute(req, res) {
const query = url.parse(req.url, true).query;
const etat = query.etat ?? 1;
const band = query.band ?? null;
const year = query.year ?? null;
db.query(sql, [etat, band, year], (err, results) => {
if (err) {
res.writeHead(500);
res.end('Erreur serveur');
return;
}
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify(results));
});
}On a donc ici une première mise en œuvre fonctionnelle de GET, dans laquelle :
server.jsreconnaît correctement la route grâce aupathnamealbums.jsrécupère les paramètres viaquery- la requête SQL reçoit ensuite ces valeurs dans le tableau de binding

Cette approche reste simple, lisible, et surtout compatible avec l’architecture que nous avons construite jusque là. Elle montre aussi qu’avec un serveur Node.js écrit sans framework, la transmission des paramètres ne pose pas seulement la question de la requête SQL, mais aussi celle du routage en amont.
Transmettre les paramètres avec la méthode POST
Jusqu’ici, nous avons utilisé GET pour faire transiter des paramètres dans l’URL. Cette approche est très pratique pour tester depuis un navigateur, car tout est visible et modifiable rapidement. Elle mélange cependant deux niveaux différents : l’URL de la page (hôte) et l’appel à l’API.
Dans la pratique, il est essentiel de bien distinguer deux transmissions de paramètres, car elles n’interviennent pas au même moment et ne jouent pas le même rôle dans le fonctionnement global de l’application. Confondre ces deux étapes conduit souvent à des incompréhensions, notamment lorsqu’on passe de tests simples à une logique d’API plus structurée.
- d’une part, la page hôte → le script : lorsque le navigateur charge une page comme
index.html?etat=1&band=4, ces paramètres font partie de l’URL de la page affichée. Le script peut les lire viawindow.location.search. Cette étape est généralement faite en GET, car elle reste visible, modifiable à la main, et donc très pratique pour tester différents cas sans toucher au code. - d’autre part, le script → l’API : une fois ces paramètres récupérés, le script décide comment les transmettre à l’API via
fetch. À ce niveau, on n’est plus dans l’URL de la page, mais dans une requête HTTP distincte. On peut alors choisir entre GET (paramètres dans l’URL) ou POST (paramètres dans le body), selon le besoin. Cette seconde transmission est celle qui pilote réellement la requête côté serveur et le binding SQL.
Dans ce chapitre, nous nous concentrons donc sur la seconde transmission : envoyer les données de fetch vers l’API en POST. Bien entendu, nous porurions également transmettre les paramètres de la page hôte en POST, mais pour conserver des tests simples directement depuis le navigateur, nous gardons ici une lecture en GET côté page, puis un envoi en POST vers l’API.
Côté server.js, aucune modification n’est nécessaire si la séparation entre pathname et query a déjà été mise en place au chapitre précédent. Le routage reste identique. La différence se situe dans la manière dont la route lit les données (se baser sur server_II_05.js).
Adapter l’appel fetch
Nous allons maintenant modifier l’appel fetch pour envoyer les paramètres dans le corps de la requête (body), au format JSON. Contrairement à GET, où les données sont visibles dans l’URL, POST permet d’envoyer des informations plus structurées, sans dépendre du format de la query string. C’est également l’approche la plus courante dès que l’on manipule des objets, des formulaires ou des filtres multiples.
Si ce mécanisme vous semble nouveau, vous pouvez consulter la documentation officielle qui détaille le fonctionnement de fetch, les options disponibles et les différents modes d’envoi de données, Utiliser l’API Fetch sur MDN.
// script_II_05.js
const params = new URLSearchParams(window.location.search);
const etat = params.get('etat') ?? 1;
const band = params.get('band') ?? '';
const year = params.get('year') ?? '';
fetch('/api/albums', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// les paramètres sont ici sérialisés en JSON dans le body
body: JSON.stringify({ etat, band, year })
})
.then(r => r.json())
.then(data => {
// ici la suite du code (affichage)
})
.catch(err => console.error(err));Le principe ne change pas : on lit des paramètres côté page, puis on les transmet à l’API. La différence est simplement le canal de transmission (body au lieu de query string).
Lire le body côté API
À l’arrivée côté API, les paramètres ne sont plus présents dans l’URL, mais dans le corps de la requête. Node.js ne les expose pas directement : il faut lire le flux de données (data), puis reconstruire le contenu.
// albums_II_05.js
export default function albumsRoute(req, res) {
if (req.method === 'POST') {
let body = '';
// À chaque chunk reçu, on ajoute son contenu à la variable body
// Le body arrive sous forme de flux (stream), et non en une seule fois
req.on('data', chunk => {
body += chunk.toString();
});
// L'événement 'end' est déclenché lorsque tout le corps de la requête a été reçu
// On peut alors traiter les données complètes
req.on('end', () => {
// Le body est une chaîne JSON → on la convertit en objet JavaScript
// On prévoit '{}' par défaut pour éviter une erreur si le body est vide
const data = JSON.parse(body || '{}');
const etat = data.etat ?? 1;
const band = data.band ?? null;
const year = data.year ?? null;
db.query(sql, [etat, band, year], (err, results) => {
if (err) {
res.writeHead(500);
res.end('Erreur serveur');
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(results));
});
});
return;
}
// fallback GET (utile pour tests rapides)
res.writeHead(405);
res.end('Méthode non autorisée');
}
À retenir
POST ne change pas la requête SQL : il change uniquement la manière dont les données sont transmises à l’API. On ne lit plus une query string dans l’URL, mais un corps JSON, que l’on reconstruit puis que l’on injecte dans la requête via le même mécanisme de binding.
Paramètres nommés et binding
Depuis le début de ces exemples, nous avons systématiquement utilisé des ? comme placeholders. Cette écriture fonctionne très bien et reste la plus répandue. Elle impose toutefois une contrainte implicite : l’ordre des valeurs dans le tableau doit correspondre exactement à l’ordre des ?dans la requête. Dès que la requête s’allonge, ou que l’on manipule plusieurs paramètres, cette correspondance devient moins lisible et plus sujette à erreur.
Les paramètres nommés apportent une alternative simple : au lieu de s’appuyer sur l’ordre, on associe chaque valeur à un nom explicite. On ne “devine” plus la correspondance, on la déclare.
Mettre en place les paramètres nommés
Avec mysql2, il est possible d’activer l’option namedPlaceholders dès la création de la connexion. Cette option indique au moteur que les requêtes pourront utiliser des paramètres nommés (préfixés par :) plutôt que de simples ?, et qu’il devra faire la correspondance automatiquement entre les noms utilisés dans la requête et les valeurs fournies dans le code :
import mysql from 'mysql2';
const db = mysql.createConnection({
host: 'localhost',
user: 'root',
password: '',
database: 'dbname',
namedPlaceholders: true
});Écrire la requête avec des noms
On remplace les ? par des identifiants précédés de :. Chaque nom correspond explicitement à une valeur attendue, ce qui permet de rendre la requête plus lisible et de comprendre immédiatement à quoi sert chaque paramètre, sans dépendre de l’ordre dans lequel les valeurs sont transmises.
SELECT ALBUMS.*, BANDS.ch_band_label
FROM tab_albums AS ALBUMS
JOIN tab_bands AS BANDS ON ALBUMS.ch_album_band_id = BANDS.ch_band_id
WHERE ALBUMS.ch_album_etat = :etat
AND ALBUMS.ch_album_band_id = :band
AND ALBUMS.ch_album_year = :yearTransmettre les valeurs
Au lieu d’un tableau, on passe un objet dont les clés correspondent aux noms utilisés dans la requête. Cette approche permet de faire le lien de manière explicite entre chaque valeur et son usage dans la requête SQL : on ne dépend plus d’une position dans un tableau, mais d’un nom clairement identifié, ce qui rend l’intention du code plus lisible et plus facile à maintenir :
const params = {
etat: 1,
band: 4,
year: 1973
};
db.query(sql, params, (err, results) => {
// suite du traitement
});Chaque propriété de l’objet est automatiquement associée au placeholder portant le même nom. Cette correspondance se fait de manière directe : la clé etat alimente :etat, band alimente :band, etc. On n’est plus dans une logique implicite basée sur la position, mais dans une relation explicite entre le code JavaScript et la requête SQL.
Cela ouvre également la porte à des usages plus souples : on peut construire dynamiquement un objet de paramètres, ajouter ou retirer des propriétés en fonction du contexte, ou encore réutiliser une même structure dans plusieurs requêtes sans avoir à recalculer un ordre précis. L’ordre n’a donc plus d’importance, ce qui réduit fortement les erreurs liées aux décalages de paramètres et facilite les évolutions du code dans le temps.
Lecture et bénéfices
Cette écriture devient particulièrement intéressante lorsque :
- la requête contient plusieurs paramètres
- certains paramètres sont optionnels
- on souhaite faire évoluer la requête sans revoir l’ordre du tableau
On gagne en lisibilité (on comprend immédiatement à quoi correspond chaque valeur) et en robustesse (moins d’erreurs liées au décalage d’index). À ce stade, le mécanisme reste identique dans son principe : on prépare une requête, puis on injecte des valeurs. Seule la forme change, mais cette forme peut grandement faciliter la maintenance dès que le code grandit.
À propos du moteur de requêtes (mysql2)
Le mécanisme de binding utilisé dans les chapitres précédents s’appuie sur le module mysql2, qui gère nativement les requêtes paramétrées et leur exécution. Il prend en charge la préparation de la requête, l’injection sécurisée des valeurs et l’envoi vers la base de données. Autrement dit, on ne concatène plus des chaînes SQL à la main. On décrit une requête, puis on lui fournit des valeurs. Cette séparation entre structure et données rend le code plus lisible, mais surtout plus sûr. Pour approfondir ce fonctionnement :
- Le dépôt officiel du projet, qui permet de comprendre l’architecture du module et de suivre son évolution : sidorares/node-mysql2
- https://github.com/sidorares/node-mysql2
- La documentation complète, qui détaille les options disponibles (placeholders, connexions, promesses…) et les usages avancés : MySQL2 – Documentation
Séparer davantage la logique
Jusqu’ici, la route (albums.js) gère à la fois : la lecture des paramètres, la requête SQL, et la réponse HTTP. Cela reste lisible tant que le code est court. Mais dès que les requêtes se multiplient (plusieurs filtres, jointures, variantes), ce mélange devient difficile à maintenir. L’objectif de ce chapitre est d’introduire un découpage simple :
- la route s’occupe de HTTP : elle reçoit la requête entrante, extrait et valide les paramètres, choisit le bon traitement, puis formate la réponse (codes HTTP, en-têtes, JSON). Elle orchestre le flux sans contenir la logique métier.
- un service s’occupe des données : il construit la requête SQL, applique le binding des paramètres, exécute l’appel à la base et retourne un résultat exploitable. Il encapsule la logique métier liée aux données, indépendamment du protocole HTTP.
Ce n’est pas un changement d’outil, mais un changement d’organisation. On prépare ainsi une montée en charge progressive, sans complexifier l’architecture.
Extraire la requête dans un service
On crée un dossier servicesà la racine du projet, destiné à regrouper l’ensemble des traitements liés aux données. On y place ensuite des fichiers dédiés, par exemple services/albumsService.js, chacun contenant des fonctions responsables d’une partie précise de la logique métier. Ce dossier a vocation à s’enrichir au fur et à mesure du projet, avec par exemple : albumsService.js pour les albums, bandsService.js pour les groupes, ou encore usersService.js pour la gestion des utilisateurs. Chaque service reste ciblé, ce qui permet de structurer progressivement le code sans le complexifier. Le fichier albumsService.js contient ici une fonction responsable de la requête.
// services/albumsService_II_06.js
export function getAlbumsByEtat(db, etat, callback) {
const sql = `
SELECT ALBUMS.*, BANDS.ch_band_label
FROM tab_albums AS ALBUMS
JOIN tab_bands AS BANDS ON ALBUMS.ch_album_band_id = BANDS.ch_band_id
WHERE ALBUMS.ch_album_etat = ?
`;
// trace d'entrée dans le service
console.log('[Service] getAlbumsByEtat - params:', { etat });
db.query(sql, [etat], (err, results) => {
if (err) {
console.error('[Service] erreur SQL:', err);
return callback(err);
}
// trace de sortie du service
console.log('[Service] résultats récupérés:', results.length);
callback(null, results);
});
}Points clés :
- la fonction est exportée avec
export: cela la rend accessible depuis d’autres fichiers du projet. On peut ainsi la réutiliser dans plusieurs routes sans dupliquer le code, ce qui devient rapidement essentiel dès que l’on multiplie les points d’entrée vers l’API. - elle reçoit
dbet les paramètres nécessaires : le service reste volontairement générique. Il ne dépend pas d’un contexte particulier, mais uniquement des données dont il a besoin pour exécuter la requête. Cela le rend plus souple et plus facile à tester. - elle ne connaît rien de HTTP (pas de
req, pas deres) : c’est un point important. Le service ne sait pas comment la requête est arrivée, ni comment la réponse sera envoyée. Il se concentre uniquement sur l’accès aux données, ce qui évite de mélanger les responsabilités. - elle renvoie le résultat via un callback : une fois la requête exécutée, le service transmet simplement le résultat (ou une erreur) à la fonction appelante. C’est ensuite la route qui décide comment interpréter ce résultat et quelle réponse HTTP renvoyer.
Importer et utiliser le service dans la route
Dans api/albums.js, on importe cette fonction et on l’utilise.
// api/albums_II_06.js
import { db } from '../db.js';
import { getAlbumsByEtat } from '../services/albumsService.js';
export default function albumsRoute(req, res) {
// récupération simplifiée du paramètre
const etat = 1;
// trace côté route avant appel du service
console.log('[Route] appel du service avec etat =', etat);
getAlbumsByEtat(db, etat, (err, results) => {
if (err) {
console.error('[Route] erreur reçue du service');
res.writeHead(500);
res.end('Erreur serveur');
return;
}
// trace côté route après retour du service
console.log('[Route] réponse prête, nombre de résultats :', results.length);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(results));
});
}Cette séparation entre route et service n’est pas visible côté navigateur. Pour les DevTools, rien ne change : une requête est envoyée, une réponse est reçue. Toute l’organisation que nous mettons en place ici concerne uniquement le serveur et vise à améliorer la lisibilité, la maintenance et l’évolution du code.
Pour rendre ce fonctionnement plus concret, les console.log ajoutés dans le service et dans la route permettent de suivre le cheminement réel d’une requête côté serveur : on voit le passage dans la route, l’appel au service, puis le retour des résultats. Ces traces s’affichent dans la console Node.js (le terminal dans lequel le serveur est lancé), et non dans la console du navigateur. Elles constituent un repère simple pour comprendre l’enchaînement des traitements, en complément de ce que montrent les DevTools.

À retenir :
L’import de la fonction via import { ... } permet de faire circuler clairement les responsabilités entre les fichiers. La route ne porte plus la logique de la requête, elle se contente d’orchestrer l’échange HTTP et délègue le travail au service. Cette séparation rend immédiatement le code plus lisible, car chaque fichier exprime un rôle précis.
Dans cette organisation, la gestion HTTP reste centralisée dans la route. C’est elle qui reçoit la requête, décide du traitement à appliquer, puis formate la réponse. Le service, de son côté, se concentre uniquement sur l’accès aux données. Cette répartition évite de mélanger les responsabilités et facilite la compréhension globale du flux.
Pourquoi ce découpage est utile
Ce découpage devient particulièrement intéressant dès que le projet commence à s’étoffer. En isolant la logique liée aux données, on obtient un code plus lisible, où chaque fichier a une intention claire. Il devient également plus simple de réutiliser une même requête dans plusieurs contextes, sans avoir à la dupliquer.
Au fil du temps, cette organisation facilite aussi les évolutions. Ajouter un nouveau filtre, une variante de requête ou un nouveau point d’entrée ne surcharge plus la route existante : on étend les services de manière progressive. Le code reste structuré, même lorsque le nombre de fonctionnalités augmente.
Plus largement, ce type de découpage constitue une première étape vers des architectures plus avancées, sans introduire de complexité inutile. On ne change pas les outils, on améliore simplement la manière de les organiser. C’est souvent à ce moment que le code devient réellement confortable à lire, à maintenir et à faire évoluer.
Gérer les erreurs et les statuts HTTP
Une API ne renvoie pas uniquement des données. Elle raconte aussi ce qu’il se passe. Lorsqu’une requête arrive, le serveur ne doit pas seulement répondre, il doit indiquer clairement si tout s’est bien déroulé, si quelque chose pose problème, ou si la demande n’est pas valide. C’est précisément le rôle des codes HTTP. Dans une logique d’API, ces statuts deviennent un véritable fil conducteur entre le serveur et le client. Ils permettent de comprendre immédiatement la nature de la réponse, sans même analyser le contenu JSON.

On retrouve bien sûr des statuts courants comme 200 pour un succès, 400 pour une requête incorrecte, 404 lorsqu’une ressource est introuvable, ou encore 500 lorsqu’une erreur interne survient. Mais au-delà de ces valeurs, ce qui compte réellement, c’est la cohérence de leur usage. L’enjeu n’est donc pas simplement de renvoyer un code, mais de construire une réponse exploitable, compréhensible, et cohérente avec la situation rencontrée.
Exemple simple de validation
Avant même d’interroger la base de données, il est souvent pertinent de vérifier les paramètres reçus. Cela permet d’intercepter les erreurs le plus tôt possible, sans mobiliser inutilement la couche SQL.
if (!etat || isNaN(etat)) {
res.writeHead(400, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
message: 'Paramètre invalide'
}));
return;
}Dans cette situation, on bloque immédiatement le traitement. On évite une requête inutile, et surtout on retourne une information claire, directement exploitable côté client. Le message n’est pas seulement technique, il devient lisible.
Exemple d’erreur serveur
if (err) {
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
message: 'Erreur serveur'
}));
return;
}Ici, on est dans un autre registre. L’erreur ne vient pas de la requête, mais du traitement lui-même. Cette distinction est essentielle, car elle permet au client de réagir différemment selon le contexte.
Lecture côté client
Côté navigateur ou application, cette distinction prend tout son sens. Une erreur de saisie pourra être corrigée immédiatement, alors qu’une erreur serveur nécessitera plutôt une gestion différente, voire une tentative ultérieure. On ne se contente plus d’envoyer des données. On met en place une communication structurée, capable de porter du sens.
Pour approfondir le rôle précis des codes HTTP et comprendre dans quels contextes utiliser chacun d’eux, vous pouvez vous appuyer sur la documentation de référence proposée par MDN, qui détaille de manière complète les différents statuts et leurs usages : Codes de statut de réponse HTTP.
Structurer les réponses JSON
Jusqu’ici, nous avons souvent renvoyé directement les résultats de la requête SQL. Cela fonctionne parfaitement dans un premier temps, mais montre rapidement ses limites dès que l’on souhaite faire évoluer l’API. Plutôt que de renvoyer un tableau brut, il devient intéressant de structurer la réponse. Non pas pour complexifier, mais pour donner un cadre stable dans lequel les données pourront évoluer.
Réponse de succès
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: true,
data: results,
count: results.length
}));Ici, les données restent présentes, mais elles sont intégrées dans une structure plus explicite. Le champ success permet de savoir immédiatement si tout s’est bien passé, data contient les résultats, et count apporte une première information complémentaire. Ce type d’enveloppe ne change rien au fonctionnement immédiat, mais prépare déjà les évolutions futures.
Réponse d’erreur
res.writeHead(500, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
success: false,
message: 'Erreur serveur'
}));Même en cas d’erreur, on conserve une structure cohérente. Cela évite au client d’avoir à gérer des formats différents selon les situations, et lui permet de s’appuyer sur des repères stables pour interpréter la réponse. Que la requête réussisse ou échoue, la logique reste la même, ce qui simplifie considérablement le traitement côté front et limite les cas particuliers à gérer.
Pourquoi c’est important
Avec ce type d’organisation, on pose les bases d’une API plus stable. Le front n’a plus besoin d’interpréter des structures variables, et le serveur peut enrichir ses réponses sans casser l’existant. C’est aussi ce qui permettra, plus tard, d’ajouter des éléments comme la pagination, des filtres appliqués, ou des informations contextuelles. On ne remplace pas la structure, on la fait évoluer progressivement.
Cette manière de structurer les réponses ne sort pas de nulle part. Elle s’inscrit dans une logique plus large de conception d’API REST, où chaque échange doit être cohérent, lisible et prévisible pour le client. Si vous souhaitez prendre un peu de recul sur cette approche, vous pouvez consulter cet article qui revient sur les principes fondamentaux du REST et leur mise en application dans des projets concrets : Comprendre le concept de REST pour le développement d’applications web efficaces.
Évolution de l’API URL : remplace url.parse()
Dans les chapitres précédents, nous avons utilisé url.parse() pour extraire les paramètres d’une URL. Avant même d’aborder son évolution, il est intéressant de rappeler que la manière dont nous construisons nos routes et manipulons les paramètres d’URL s’inscrit dans une logique plus globale de conception d’API. Une URL comme /api/albums n’est pas simplement un chemin technique : elle représente une ressource, et la façon dont on y associe des paramètres (query string, filtres…) participe directement à la lisibilité et à la cohérence de l’API.
Ces notions sont au cœur des bonnes pratiques REST, notamment sur la manière de structurer les endpoints et de rendre les URLs compréhensibles et prévisibles. Vous pouvez approfondir ce point dans cet article dédié : Optimisation des APIs RESTful : URLs & Bonnes pratiques.
Dans ce contexte, url.parse() n’est qu’un outil parmi d’autres pour exploiter ces informations. Cette méthode a longtemps été la référence dans Node.js, et elle reste encore fonctionnelle aujourd’hui. Mais elle appartient désormais à une génération d’API en cours de retrait. Progressivement, Node.js s’aligne sur les standards du web en adoptant l’API URL, déjà utilisée dans les navigateurs. Cette évolution n’est pas anodine. Elle permet d’unifier les pratiques entre le front et le serveur.

Ancienne approche
// albums_II_07.js
import url from 'url';
const parsedUrl = url.parse(req.url, true);
const query = parsedUrl.query;
// ou
const query = url.parse(req.url, true).query;
// et dans server_II_07.js
// avec l’API WHATWG, req.url est une URL relative (ex: "/api/albums?...")
// on doit fournir une base absolue ; on utilise ici l’hôte de la requête
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
// on peut ensuite utiliser parsedUrl.pathname comme précédemment
// et accéder aux paramètres via parsedUrl.searchParamsD’ailleurs, les versions récentes de Node.js affichent désormais un avertissement explicite dans la console signalant que url.parse() est déprécié et peut présenter des comportements non standardisés, voire des implications en matière de sécurité. Ce message, que l’on retrouve par exemple lors de l’exécution du serveur, invite à migrer vers l’API WHATWG URL, devenue la référence. Pour comprendre en détail les raisons de cette évolution et les différences de comportement, vous pouvez également consulter la documentation officielle de Node.js : url.parse(urlString[, parseQueryString[, slashesDenoteHost]]).

Nouvelle approche recommandée
// albums_II_07.js
// nous pouvons retirer import url from 'url'; qui n'est plus nécessaire
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);
const query = Object.fromEntries(parsedUrl.searchParams.entries());
// et dans server_II_07.js
const parsedUrl = new URL(req.url, `http://${req.headers.host}`);Ici, on adopte une logique plus proche de ce que l’on connaît déjà côté navigateur. L’URL devient un véritable objet JavaScript que l’on peut manipuler simplement, comme n’importe quelle autre structure. Concrètement, new URL(req.url, ...) crée un objet qui contient différentes propriétés : le chemin (pathname), les paramètres (searchParams), l’origine, etc. On ne manipule plus une chaîne de caractères brute, mais un objet structuré.
La partie searchParams est elle-même un objet dédié à la gestion des paramètres d’URL. Il propose des méthodes comme .get(), .set() ou .entries() pour lire et manipuler les valeurs. Dans l’exemple précédent :
const query = Object.fromEntries(parsedUrl.searchParams.entries());searchParams.entries()retourne une liste de couples clé/valeur. Par exemple, pour une URL comme/api/albums?etat=1&band=4, on obtient une structure équivalente à :[['etat', '1'], ['band', '4']]. Chaque entrée correspond à un paramètre présent dans l’URL.Object.fromEntries()transforme cette liste en objet JavaScript classique. À partir de l’exemple précédent, on obtient alors :{ etat: '1', band: '4' }, ce qui permet de manipuler les paramètres beaucoup plus simplement dans le code.
On obtient ainsi un objet directement exploitable dans le code, sans avoir à parser manuellement la chaîne. Si ces notions vous semblent nouvelles, elles reposent en réalité sur des mécanismes JavaScript classiques (objets, tableaux, itérations). Vous pouvez approfondir ces bases dans cet article : Parcourir et manipuler des objets efficacement avec JavaScript. On ne fait donc rien de magique ici. On applique simplement des outils JavaScript standards à la manipulation d’une URL.
Mise en perspective
Côté front, nous avons pris l’habitude de lire les paramètres directement dans l’URL de la page avec URLSearchParams, une interface native du navigateur dédiée à la manipulation des query strings. Elle permet d’accéder simplement aux paramètres (.get()), d’en ajouter (.set()), de les parcourir (.entries()), ou encore de reconstruire une chaîne de requête. Autrement dit, on ne « parse » plus une chaîne à la main : on s’appuie sur un objet conçu pour ça. Pour découvrir l’ensemble des méthodes disponibles et leurs usages, vous pouvez consulter la documentation MDN : URLSearchParams. Concrètement, on l’utilise ainsi :
new URLSearchParams(window.location.search)
// window.location.search correspond à la partie de l’URL située après le "?", par exemple "?etat=1&band=4"Ce geste devient presque naturel : on récupère une valeur, on l’utilise, et on passe à la suite. Côté serveur, avec l’API URL, on se retrouve finalement dans une situation très proche. On ne manipule plus une chaîne brute, mais un objet qui expose les mêmes mécanismes :
parsedUrl.searchParams.get('etat')Ce qui change ici, ce n’est pas seulement la syntaxe. C’est le fait de retrouver les mêmes repères des deux côtés. On lit une URL de la même manière dans le navigateur et dans Node.js, avec les mêmes méthodes et les mêmes réflexes. Au final, on ne fait pas qu’adopter une nouvelle API. On simplifie notre manière de penser le code, en réduisant l’écart entre front et serveur. Cette continuité rend l’ensemble plus cohérent, et surtout plus facile à appréhender lorsque l’on passe d’un environnement à l’autre.
À retenir
url.parse()continue de fonctionner, et rien n’impose de le remplacer immédiatement, surtout dans un contexte existant. Mais anticiper cette évolution permet de garder un code cohérent avec les standards actuels. C’est une transition naturelle, plus qu’une rupture, qui s’inscrit dans une logique d’alignement global entre client et serveur.
Conclusion
Au fil de cet article, nous avons fait évoluer un serveur Node.js simple vers une API capable de recevoir des paramètres, structurer ses réponses et organiser son code. Plus que les techniques elles-mêmes, c’est une manière de penser qui s’installe progressivement : une requête arrive, elle est interprétée, transformée, puis renvoyée sous une forme exploitable.
Ce basculement est essentiel. On ne manipule plus seulement des fichiers ou des routes, mais des échanges entre un client et une API. Cette logique reste valable quel que soit l’environnement utilisé. Les outils peuvent changer, mais la structure des échanges, elle, reste la même.
À ce stade, le serveur est capable de fournir des données de manière fiable et structurée. Il devient alors naturel de se tourner vers l’autre versant de l’application : l’interface.
La suite de cette série se concentrera donc sur la partie HTML et JavaScript côté navigateur. Nous verrons comment exploiter ces données, les afficher, les filtrer, et créer des interactions plus dynamiques. L’objectif sera de passer d’une API fonctionnelle à une interface réellement utilisable, où les données prennent forme et deviennent manipulables.
On ne change pas de logique, on la prolonge. L’API reste le socle, l’interface vient lui donner vie.
