Aller au contenu

Express : API JSON

Exemple d’API REST pour une ressource clients. Les accès à la base de données sont décrits dans PostgreSQL, MariaDB ou SQLite.

Action Méthode et chemin Succès Erreurs
Lister GET /api/clients 200
Lire GET /api/clients/:id 200 404
Créer POST /api/clients 201 400, 409
Remplacer PUT /api/clients/:id 200 400, 404
Modifier PATCH /api/clients/:id 200 400, 404
Supprimer DELETE /api/clients/:id 204 404

Réponses d’erreur homogènes :

{ "erreur": "email invalide", "champ": "email" }

src/routes/clients.js :

import { Router } from 'express';
export const routeur = Router();
const clients = new Map();
let prochainId = 1;
routeur.get('/', (req, res) => {
const page = Math.max(1, Number(req.query.page) || 1);
const taille = Math.min(100, Math.max(1, Number(req.query.taille) || 20));
const tous = [...clients.values()];
res.json({
page,
taille,
total: tous.length,
donnees: tous.slice((page - 1) * taille, page * taille),
});
});
routeur.get('/:id', (req, res) => {
const client = clients.get(Number(req.params.id));
if (!client) return res.status(404).json({ erreur: 'client introuvable' });
res.json(client);
});
routeur.post('/', (req, res) => {
const { nom, email } = req.body ?? {};
if (typeof nom !== 'string' || nom.trim() === '') {
return res.status(400).json({ erreur: 'nom obligatoire', champ: 'nom' });
}
if (typeof email !== 'string' || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) {
return res.status(400).json({ erreur: 'email invalide', champ: 'email' });
}
const client = { id: prochainId++, nom: nom.trim(), email };
clients.set(client.id, client);
res.status(201).location(`/api/clients/${client.id}`).json(client);
});
routeur.delete('/:id', (req, res) => {
if (!clients.delete(Number(req.params.id))) {
return res.status(404).json({ erreur: 'client introuvable' });
}
res.sendStatus(204);
});

Le stockage en mémoire disparaît à l’arrêt du processus : il sert aux essais. Utiliser une base de données pour des données réelles.

Valider toutes les données reçues (corps, paramètres, chaîne de requête) avec une bibliothèque de schémas :

Fenêtre de terminal
npm install zod
import { z } from 'zod';
const schemaClient = z.object({
nom: z.string().trim().min(1).max(100),
email: z.string().email(),
});
function valider(schema) {
return (req, res, next) => {
const resultat = schema.safeParse(req.body);
if (!resultat.success) {
return res.status(400).json({
erreur: 'données invalides',
details: resultat.error.issues.map((i) => ({
champ: i.path.join('.'),
message: i.message,
})),
});
}
req.body = resultat.data;
next();
};
}
routeur.post('/', valider(schemaClient), (req, res) => {
// req.body est validé et nettoyé
res.status(201).json({ id: 1, ...req.body });
});

Exemple avec PostgreSQL et le paquet pg :

Fenêtre de terminal
npm install pg
import pg from 'pg';
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL,
max: 10,
});
routeur.get('/:id', async (req, res) => {
const { rows } = await pool.query(
'SELECT id, nom, email FROM clients WHERE id = $1',
[Number(req.params.id)],
);
if (rows.length === 0)
return res.status(404).json({ erreur: 'client introuvable' });
res.json(rows[0]);
});

Les erreurs de base de données remontent au gestionnaire d’erreurs d’Express 5 sans try/catch.

import { routeur as clientsV1 } from './routes/clients.js';
app.use('/api/v1/clients', clientsV1);

Préfixer par une version permet de faire évoluer l’API sans casser les clients existants.

Fenêtre de terminal
curl -s -X POST http://127.0.0.1:3000/api/clients \
-H 'Content-Type: application/json' \
-d '{"nom":"Alice","email":"alice@exemple.com"}'
curl -s 'http://127.0.0.1:3000/api/clients?page=1&taille=10'
curl -i -X DELETE http://127.0.0.1:3000/api/clients/1

Node.js intègre un lanceur de tests (node --test) ; fetch est disponible sans installation.

test/api.test.js :

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { app } from '../src/app.js';
test('GET /sante répond ok', async (t) => {
const serveur = app.listen(0, '127.0.0.1');
t.after(() => serveur.close());
const { port } = serveur.address();
const reponse = await fetch(`http://127.0.0.1:${port}/sante`);
assert.equal(reponse.status, 200);
assert.deepEqual(await reponse.json(), { statut: 'ok' });
});
Fenêtre de terminal
node --test

Le port 0 laisse le système choisir un port libre : les tests ne rentrent pas en conflit.

Symptôme Piste
req.body est undefined express.json() absent, ou requête sans Content-Type: application/json
SyntaxError: Unexpected token dans les journaux Corps JSON invalide : le gestionnaire d’erreurs renvoie 400 si err.type === 'entity.parse.failed'
PayloadTooLargeError Augmenter limit avec prudence
Id non numérique accepté Valider les paramètres de chemin
Connexion à la base refusée DATABASE_URL, serveur démarré, droits (voir PostgreSQL : utilisateurs et droits)
Les tests ne se terminent pas Serveur non fermé : t.after(() => serveur.close())

Ubuntu 24.04 et 26.04 : différences à connaître

Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »

Aucune propre à la distribution. node --test et fetch demandent Node.js 18 ou plus récent.