Construire une API REST robuste avec Express et Zod

Une API REST robuste ne se résume pas à une pile de routes qui répondent en 200. Chaque requête doit être comprise, contrôlée, autorisée puis exécutée sans exposer d’état interne. Express organise ces étapes, tandis qu’un schéma Zod transforme les données reçues en formes fiables et prévisibles.

Le bon point de départ consiste à définir le contrat avant le gestionnaire : paramètres, corps, réponses et erreurs. Cette discipline réduit les entrées invalides, simplifie les tests et permet à TypeScript de refléter l’évolution du projet sans multiplier les contrôles dispersés.

Définir les contrats et les routes

Décrivez les ressources avec des URL stables et des verbes adaptés : GET pour lire, POST pour créer, PATCH pour modifier et DELETE pour supprimer. Une création réussie renvoie 201 et l’URL de la ressource ; une suppression renvoie 204 si aucun corps n’est nécessaire.

Prévoyez pagination, filtres, corps vide, ressource absente et mises à jour concurrentes. Un contrat d’erreur uniforme associe un code stable, un message court et un identifiant de requête, sans révéler de détail interne du serveur.

Le runtime influence peu la validation, mais il conditionne l’outillage. Cette comparaison des runtimes JavaScript vérifie les compatibilités entre Node.js, Deno et Bun. Pour Express, choisissez une version maintenue, un support stable des dépendances et des outils d’observabilité disponibles.

Structurer le serveur et ses middlewares

Organisez le projet par responsabilité : routes, schémas, services, accès aux données et configuration. Un dossier schemas centralise les contrats Zod, tandis qu’un dossier middlewares accueille validation, authentification et gestion des erreurs. Les contrôleurs restent ainsi courts et testables.

Construisez un pipeline déterministe : identifiant de requête, limite du corps, en-têtes de sécurité, authentification, validation, règle métier, puis gestionnaire d’erreur. Les ressources pour développeurs web donnent des repères pour comparer les middlewares Express avant de retenir ceux qui correspondent au service.

Externalisez la configuration dans des variables d’environnement validées au démarrage. Refusez de lancer le processus si une valeur secrète, un port ou une URL de connexion manque. À l’arrêt, cessez d’accepter des requêtes, laissez les appels en cours se terminer, puis fermez le pool de données.

Valider les entrées avec les schémas Zod

Un schéma Zod décrit précisément ce que l’API accepte. Utilisez des schémas distincts pour le corps, la chaîne de requête et les paramètres d’URL, avec un mode strict lorsque les champs inconnus doivent être rejetés.

const utilisateurSchema = z.object({
  email: z.string().trim().email(),
  nom: z.string().trim().min(2).max(80),
  role: z.enum(["lecteur", "admin"]).default("lecteur"),
}).strict();

Validez avant la logique métier. safeParse distingue les données conformes des autres sans interrompre le processus par une exception. Pour un corps malformé, répondez 400 ; pour un corps syntaxiquement valide mais contraire au schéma, utilisez 422. La réponse contient code, message et details avec le chemin de chaque champ, jamais la valeur brute si elle contient un secret. Trois stratégies permettent d’arbitrer entre vérification à l’exécution, standard portable et code manuel.

Stratégie Données contrôlées Erreurs exploitables Typage et maintenance
Zod Corps, requête et paramètres Issues structurées avec chemin Type inféré et règles centralisées
JSON Schema Formats standard et portables Erreurs fournies par un validateur externe Compatible avec plusieurs outils
Contrôle manuel Champs choisis par le développeur Réponses verbales à écrire soi-même Code répétitif et dérive rapide

Le schéma Zod reste cohérent avec TypeScript, car le type de sortie est inféré et les règles ne sont pas dupliquées. Le contrôle ressemble au principe d’un test de keno : une saisie invalide doit bloquer tout effet de bord. Combinée aux contraintes de base et aux règles métier, la validation forme une défense en profondeur.

Sécuriser les données et les transactions

Après validation, la base de données doit encore garantir l’intégrité. Les guides de bases de données couvrent les migrations, index et contraintes à associer aux schémas applicatifs. Une clé étrangère, une colonne unique ou une valeur permissible au niveau SQL subsistent même si une autre voie contourne l’API.

Regroupez les écritures liées dans une transaction. Pour une création touchant plusieurs tables, annulez tout si une étape échoue. Ajoutez une clé d’idempotence lorsque le client peut répéter une requête POST : stockez sa réponse et renvoyez le même résultat au lieu de créer un doublon.

Protégez les performances avec une pagination bornée, des index adaptés et un chargement relationnel contrôlé. Pour les mises à jour, un numéro de version permet d’appliquer une concurrence optimiste et de rejeter une écriture fondée sur un état obsolète.

Gérer les erreurs et l’observabilité

Ajoutez helmet pour les en-têtes, une liste blanche CORS, une limite de débit et une taille maximale du corps. Ces règles limitent les abus sans remplacer l’authentification. Définissez une limitation adaptée aux points d’accès sensibles, comme la connexion ou la réinitialisation du mot de passe.

Authentifiez l’appelant, puis vérifiez son autorisation sur la ressource. Un jeton valide ne permet pas à un utilisateur de modifier le compte d’un autre. Centralisez ces vérifications dans des politiques déclaratives, testez les interdictions et ne déduisez jamais un rôle d’un corps de requête.

Centralisez les erreurs dans un middleware à quatre arguments qui distingue les erreurs prévues des incidents. Le format peut rester { error: { code, message, requestId } }. En production, masquez les piles et les détails techniques, journalisez l’incident avec l’identifiant de requête, puis mesurez la latence, le taux d’erreur et la saturation.

Automatiser les tests et le déploiement

Testez les schémas isolément avec les cas heureux, champs absents, valeurs limites et types incorrects. Ajoutez des tests d’intégration avec Supertest pour vérifier le statut, le corps et les en-têtes. Une base isolée ou une transaction enveloppée évite que les tests ne modifient les données de développement.

Exécutez cette suite à chaque fusion, puis analysez les résultats et les métriques. Construisez une image Docker reproductible, lancez les migrations avant le déploiement et prévoyez une route de santé qui vérifie réellement la base. Une ancienne version peut ensuite être retirée sans interrompre le service.

Pour passer à l’action, créez un dépôt avec le squelette Express, une route d’exemple, son schéma Zod et un test Supertest. Configurez les variables d’environnement, puis exécutez la suite avant la première publication.

Commencez par un module de faible risque, documentez son contrat et appliquez les mêmes garde-fous à chaque nouvelle route. Déployez cette première version, surveillez ses métriques et faites évoluer le contrat par des versions documentées.