Anti-patterns backend : 7 erreurs coûteuses et leurs solutions pratiques
Apprenez à repérer et à corriger sept erreurs courantes en ingénierie backend, allant des contrôleurs surchargés aux erreurs non gérées, avant qu’elles ne provoquent des problèmes en production.
Lorsque l’on commence à développer des applications backend, il est facile de penser que le véritable défi réside dans la maîtrise de davantage d’outils.
Node.js.
Express.
PostgreSQL.
Redis.
Docker.
Files d’attente de messages.
Conception de systèmes.
Mais après avoir mis en ligne un certain nombre d’applications, une vérité différente devient évidente.
Connaître plus d’outils ne fait pas automatiquement de vous un ingénieur backend plus compétent.
La majeure partie de la progression réelle provient de commettre des erreurs, d’en comprendre les raisons et de s’assurer qu’elles ne se reproduisent pas.
Voici sept erreurs courantes en backend dont il vaut la peine d’apprendre, ainsi que l’approche qui fonctionne mieux.
1. Mettre tout dans le contrôleur
C’est souvent l’une des premières erreurs que les gens commettent.
Un point d’entrée peut initialement ressembler à ceci :
app.post("/orders", async (req, res) => {
const { userId, productId, quantity } = req.body;
const user = await db.users.findUnique({
where: { id: userId }
}); if (!user) {
return res.status(404).json({
message: "User not found"
});
} const product = await db.products.findUnique({
where: { id: productId }
}); if (!product) {
return res.status(404).json({
message: "Product not found"
});
} if (product.stock < quantity) {
return res.status(400).json({
message: "Not enough stock"
});
} const order = await db.orders.create({
data: {
userId,
productId,
quantity
}
}); await sendEmail(user.email); return res.status(201).json(order);
});
Cela fonctionne.
Mais regardez tout ce que cette seule fonction gère maintenant :
- La validation
- Les requêtes de base de données
- Les règles métier
- Vérification des stocks
- Création des commandes
- L’envoi d’e-mails
- Les réponses HTTP
Lorsqu’un projet continue de grandir, les fonctions qui assument autant de responsabilités deviennent des blocs de logique impossibles à gérer.
La solution consiste à répartir les responsabilités.
Request
↓
Controller
↓
Service
↓
Repository
↓
Databas
Le rôle du contrôleur est lié à HTTP.
La couche de service gère la logique métier.
La couche de repository s’occupe de l’accès aux données.
Cela ne signifie pas qu’une petite application ait besoin de six couches d’abstraction.
Cela signifie que chaque partie du système doit avoir une tâche clairement définie.
2. Faire confiance au frontend
Cette erreur peut introduire des bugs et, pire encore, des vulnérabilités de sécurité.
Supposons que le frontend envoie ce payload :
{
"price": 10,
"quantity": 2
}
Il est tentant d’utiliser simplement le prix envoyé par le client pour calculer le montant total de la commande.
Ne le faites pas.
Rien n’empêche un client malveillant d’envoyer :
{
"price": 1,
"quantity": 100
}
Le frontend est sous le contrôle de l’utilisateur, pas du vôtre.
Votre backend doit valider et appliquer les règles qui sont vraiment importantes.
Par exemple :
const product = await productRepository.findById(
productId
);
const total = product.price * quantity;
C’est le backend, et non le client, qui doit être la source de vérité pour les prix.
Ce même principe de prudence doit s’appliquer à plusieurs autres domaines que le client pourrait tenter d’influencer :
- Rôles des utilisateurs
- Permissions
- Remises
- Stock
- Montants de paiement
- Statut du compte
- Propriété des ressources
Pensez au frontend comme à un outil permettant de façonner l’expérience utilisateur avec votre produit.
Ce n’est pas une frontière de sécurité.
3. Gestion défectueuse des erreurs
Dès les débuts, la gestion des erreurs ressemblait souvent à ceci :
try {
// something
} catch (error) {
console.log(error);
return res.status(500).json({
message: "Something went wrong"
});
}
Il n’y a rien de fondamentalement erroné à disposer d’une solution de secours universelle.
Le problème réside dans le fait de s’y fier pour toutes les situations.
L’absence d’un enregistrement utilisateur n’est pas nécessairement une erreur de niveau 500.
Une requête mal formatée n’est pas nécessairement une erreur de niveau 500.
Un e-mail déjà existant n’est pas nécessairement une erreur de niveau 500.
Votre back-end doit être capable de distinguer les différents types d’échecs.
Par exemple :
400 → Invalid request
401 → Authentication required
403 → Not allowed
404 → Resource not found
409 → Conflict
422 → Validation failure
500 → Unexpected server error
Les codes d’état spécifiques que vous choisissez dépendent des conventions de votre API, mais la cohérence est plus importante que le schéma exact.
Des réponses d’erreur structurées sont également utiles.
Par exemple :
{
"success": false,
"message": "User already exists",
"code": "USER_ALREADY_EXISTS"
}
Avec ce format, le frontend n’a pas besoin de deviner ce qui a mal tourné.
4. Codage statique de la configuration
Cette erreur semble inoffensive tant que l’on n’essaie pas de déployer.
Quelque chose comme :
const databaseUrl =
"postgresql://user:password@localhost:5432/app";
Ou bien :
const jwtSecret = "my-secret";
Évitez complètement ce schéma.
Chaque environnement dans lequel vous exécutez votre application a besoin de ses propres paramètres.
Development
↓
localhost
Staging
↓
staging databaseProduction
↓
production database
Préférez plutôt une configuration basée sur l’environnement :
DATABASE_URL=
REDIS_URL=
JWT_SECRET=
PAYMENT_API_KEY=
EMAIL_API_KEY=
Et n’insérez jamais de secrets dans le système de contrôle de version.
Un fichier .env convient pour le développement local, mais les environnements de production exigent des outils adaptés à la gestion des secrets et des configurations.
Le principe fondamental est le suivant :
Votre code ne doit pas être étroitement lié à des valeurs de configuration spécifiques à un environnement.
5. Échelle avant d’en avoir réellement besoin
C’est un piège dans lequel de nombreux développeurs tombent à un moment donné, et il est facile d’en justifier l’utilisation sur le moment.
Une équipe lance un nouveau projet et passe immédiatement à :
« Que se passera-t-il si 10 millions de personnes se présentent demain ? »
Ainsi, ils décident d’ajouter :
Microservices
Kafka
Redis
Kubernetes
Multiple databases
API Gateway
Event-driven architecture
Sur le papier, le système peut gérer une échelle massive.
Mais il n’y a en réalité que cinq utilisateurs.
Ce n’est pas une architecture solide. C’est de la complexité dont personne n’a encore besoin.
Pour la plupart des projets, il est plus judicieux de commencer simplement :
Client
↓
Node.js Application
↓
PostgreSQL
Et d’ajouter de nouvelles composantes uniquement lorsqu’il y a une raison concrète de le faire :
Need caching?
→ Redis
Need background jobs?
→ Queue + WorkerNeed more API capacity?
→ Multiple instances + Load BalancerDatabase becoming a bottleneck?
→ Optimize queries / indexes / architecture
Laissez l’architecture évoluer en fonction de besoins réels et prouvés.
N’optez pas pour un système distribué juste parce que certains vidéos affirment que c’est ce que font les ingénieurs seniors « véritables ».
6. Blocage de la demande en cas de traitement lent
Cela peut ruiner silencieusement l’expérience d’utilisation d’une API.
Imaginons cette configuration :
app.post("/order", async (req, res) => {
const order = await createOrder(); await sendEmail(); await generateInvoice(); await notifyWarehouse(); await updateAnalytics(); return res.json(order);
});
Ici, l’utilisateur attend que les cinq étapes se terminent dans l’ordre.
Même si une seule appel externe prend cinq secondes de plus, toute la réponse est alors plus lente de cinq secondes.
Une meilleure approche consiste à ne traiter que les tâches effectivement nécessaires immédiatement au sein de la demande.
Tout le reste peut être mis en file d’attente pour un traitement en arrière-plan.
Client
↓
API
↓
Create Order
↓
Queue Jobs
↓
Response
Puis vient :
Queue
↓
Worker
├── Send Email
├── Generate Invoice
├── Notification
└── Analytics
C’est précisément le type de scénario où des outils comme BullMQ associé à Redis se révèlent utiles.
Mais il y a une deuxième leçon facile à négliger ici :
Les tâches en arrière-plan doivent être idempotentes et gérer les échecs de manière appropriée.
Si une tâche s’exécute par erreur deux fois, les risques sont les suivants :
- Facturer un client plus d’une fois
- Envoyer des notifications dupliquées
- Insérer des enregistrements doubles dans la base de données
Le simple fait de transférer le travail vers une file d’attente ne résout pas ce problème à lui seul.
La tâche elle-même doit être conçue en tenant cela compte.
7. Travailler à l’aveugle en production
Cette erreur reste généralement invisible jusqu’à ce que quelque chose se brise réellement.
Imaginez qu’une API en ligne commence soudain à générer des erreurs.
À première vue, le serveur semble fonctionner normalement.
Le code aussi semble correct.
Mais voici ce qui manque :
No useful logs
No request IDs
No metrics
No error tracking
No database monitoring
À ce stade, la seule option qui reste est de deviner.
Le débogage se transforme alors en une série de haussements d’épaules :
« Peut-être que Redis est tombé en panne ? » « Peut-être que la base de données ralentit fortement ? » « Peut-être que le prestataire de paiement rencontre des problèmes ? »
C’est une situation délicate pour un ingénieur.
À tout le moins, des journaux d’activité utiles sont essentiels.
Par exemple :
{
"level": "error",
"requestId": "req_123",
"route": "/orders",
"userId": "user_456",
"message": "Payment provider timeout"
}
Avec de tels résultats, il devient possible de déterminer précisément ce qui a mal tourné et où.
Au fur et à mesure que les systèmes se développent, l’observabilité couvre généralement :
- Les journaux d’application
- Le suivi des erreurs
- L’utilisation de la CPU et de la mémoire
- Les métriques au niveau de la base de données
- Les temps de réponse des API
- La taille de la file d’attente
- Les pannes provenant des API externes
- Les points de contrôle de santé
La leçon plus importante
En y regardant de plus près, presque tous ces problèmes remontent à la même habitude fondamentale.
La question qui guide de nombreuses décisions est généralement :
"Cette fonctionnalité fonctionne-t-elle ?"
alors qu’elle devrait plutôt être :
"Est-il facile de modifier, de déboguer et d’exécuter cette fonctionnalité en production ?"
Ce changement de perspective modifie presque tout dans la manière dont le logiciel est développé.
Que vérifier avant de considérer qu’une API est terminée
Code
- Chaque couche a-t-elle une responsabilité claire et unique ?
- La logique métier peut-elle être testée de manière isolée ?
- Les contrôleurs restent-ils suffisamment légers ?
Sécurité
- Tous les données entrantes sont-elles validées ?
- Les vérifications de permissions sont-elles appliquées du côté serveur ?
- Les informations sensibles sont-elles bien protégées ?
Base de données
- Les requêtes sont-elles efficaces ?
- Existent-il les bons index ?
- Les transactions sont-elles utilisées là où c’est nécessaire ?
- Y a-t-il un problème caché lié aux requêtes N+1 ?
Prestations
- Les appels séquentiels évitables sont-ils éliminés ?
- Le travail intensif est-il reporté à un processus en arrière-plan ?
- Le cache améliorerait-il les performances ici ?
Fiableté
- Quelle est la solution de secours en cas d’échec d’une API externe ?
- Les tentatives de réessai sont-elles configurées de manière appropriée ?
- Les tâches en arrière-plan peuvent-elles être exécutées plus d’une fois sans risque ?
- Que se passe-t-il si Redis ou la base de données deviennent inaccessibles ?
Opérations
- Est-il possible de savoir ce qui s’est passé en cas d’échec ?
- Les journaux sont-ils réellement utiles ?
- Existent-il des vérifications de santé ?
- La performance d’une API peut-elle être mesurée ?
Un système sans défaut n’est pas l’objectif.
Mais il est essentiel de savoir ce qui se passe dès qu’un problème survient.
Les 7 erreurs en un coup d’œil
| Erreur | Méthode plus appropriée |
|---|---|
| Tout regroupé dans les contrôleurs | Distribuer les responsabilités entre les différentes couches |
| Faire confiance aux données provenant du frontend | Vérifier tout côté serveur |
| Gestion des erreurs uniforme pour tous les cas | Utiliser une stratégie de gestion des erreurs cohérente |
| Mots de passe codés en dur | Gestion adéquate des environnements et des configurations |
| Montée en charge trop précoce | Monter en charge en réponse aux véritables goulots d’étranglement |
| Tâches lentes bloquant les requêtes | Déplacer ces tâches en arrière-plan |
| Aucune visibilité sur le fonctionnement en production |
Conclusion finale
On pense souvent que progresser en tant que développeur backend signifie simplement acquérir davantage d’outils et de technologies.
Une vision plus précise est qu’il s’agit en réalité de comprendre les compromis à faire.
Faut-il que cette opération soit synchrone ou asynchrone ?
Ces données valent-elles la peine d’être mémorisées en cache ?
Cette requête a-t-elle besoin d’un index ?
Faut-il en faire un service distinct ?
Quel est le plan en cas de panne de Redis ?
Quel est le plan si la base de données ralentit considérablement ?
Quel est le plan si une tâche s’exécute par erreur deux fois ?
Quel est le plan si l’API externe sur laquelle on compte tombe en panne ?
Connaître les réponses à ces questions est bien plus important que de savoir simplement comment installer un autre paquet.
Car les systèmes de production ne sont pas évalués en fonction de leur comportement lorsque tout se passe bien.
C’est véritablement l’ingénierie qui se révèle au moment où les choses tournent mal.
Et chaque erreur identifiée aujourd’hui diminue le nombre d’incendies de production à éteindre plus tard.
Lectures complémentaires
- Erreurs courantes du contract API qui compromettent la fiabilité du frontend — Découvrez dix défauts récurrents dans la conception des API backend – allant de formes de réponse incohérentes à une pagination fragile – qui sapent la confiance du frontend, ainsi que les moyens de les corriger.