Structurer un backend NodeJS sans framework

Publié le

25 mars 2025

Created with Sketch.

Voici comment notre client nous avait présenté son projet : il a acheté une licence pour un logiciel destiné à gérer et automatiser des flux de production, et il voudrait qu'on lui développe un frontend par-dessus pour rendre son usage plus spécifique et plus agréable. Nous étions donc deux développeurs JavaScript assignés au projet quand a commencé la phase de spécification/conception, et il nous est très vite apparu que, contrairement à ce qu'avait imaginé le client, on aurait besoin d'un backend dédié entre le frontend et ce service tiers.

Symfony et KNP, c'est une longue histoire d'amour, et c'est notre framework privilégié pour nos backends. Mais le planning avait retenu deux développeurs JavaScript profanes, et les spécificités de ce projet collaient très bien à NodeJS. On a donc sauté sur l'occasion de démontrer à tous nos collègues l'évidente supériorité de JavaScript sur leur terrain de prédilection : le développement d'un serveur web.

Pourquoi NodeJS était un bon choix pour ce projet

NodeJS n'est pas un framework. Il ne vient donc pas avec la pelleté de fonctionnalités qu'on peut attendre d'un framework web, mais en contrepartie, permet d'avancer simplement et rapidement. Le posulat de base (qui s'est vérifié) était que notre backend ne serait pas une grosse machine, la legereté de l'outil serait donc un atout.

La grande spécificité de NodeJS en tant que serveur web tient dans les caractéristiques suivantes, a priori antinomiques : il est à la fois capable de traiter un grand nombre de requêtes simultanées, mais ne sait pas faire deux choses en même temps (monothread). Cela est dû à son implémentation basée sur une event loop et sa gestion de l'asynchrone : quand un traitement asynchrone est déclenché, NodeJS va naturellement traiter la prochaine demande à effectuer dans sa liste de tâches en attendant. NodeJS est en somme très mauvais pour effectuer lui-même des traitements lourds, mais c'est un excellent passe-plat. Et puisque nous savons déjà que les traitements lourds seront gérés par un service tiers, c'est parfait pour nous.

L'architecture, comment structurer un backend NodeJS

L'inconvénient de construire un backend sans framework, c'est qu'on ne peut se reposer sur aucune structure. Toute la responsabilité de l'organisation du code nous revient.

Je ne sais pas si c'est toujours le cas, mais a l'époque, on trouvait peu de ressources sur les architectures possibles pour une API NodeJS. On s'est donc laissé guidé par nos besoins en prenant soin dès le départ de refactoriser dès qu'un probleme structurel faisait surface, tant que le projet était petit et facile a manipuler. Voici ce qui s'est très vite mis en place :

Trois couches : points d'entrées, models, services

  • Les points d’entrée, c’est tous les moyens par lesquels on peut demander à notre serveur de réaliser des tâches. On est parti sur du GraphQL pour les endpoints, en grande partie pour son côté structurant au départ. Les autres points d’entrée étaient essentiellement des scripts, lancés depuis le serveur soit par des crons, soit en ligne de commande.
  • Les modèles, la couche de persistance, nos entités métiers sauvegardées dans notre base de données.
  • Les services, pour toute la partie I/O qui ne rentre pas dans nos modèles. On y met donc, par exemple, les échanges avec des services tiers, les modules permettant la manipulation du filesystem, etc.

L'objet Context

C'est un objet qu'on initialise depuis nos points d’entrée et par lequel on doit passer pour appeler les models, les services, et quelques autres dépendances du serveur. Ses deux atouts majeurs :

  • C'est à travers lui que l'inversion de dépendance est mise en place : il définit ce qui est accessible ou non pour un point d’entrée donné.
  • Cela évite de multiplier les paramètres sur les fonctions appelées depuis nos endpoints.

C'est dans nos tests de endpoints que son utilité était la plus évidente : chaque test peut définir avec précision comment chaque dépendance doit fonctionner. Il est aussi simple de mocker un service que de se connecter réellement à la BDD si on le souhaite. On peut brancher certains services et pas d'autres : on a la maîtrise totale sur les dépendances du serveur.

Ajout d'une quatrieme couche : les usecases

L'organisation en trois couches fonctionnait bien au départ : la productivité était bonne, les tests de endpoints exhaustifs, mais on s'est vite rendu compte qu'on avait du mal à partager de la logique entre plusieurs points d'entrés. La logique métier était d'abord placée directement dans les endpoints, puis parfois déplacée dans les modèles pour être partagée. Mais ce n'était pas pérenne, trop limitant. Il fallait que les modèles restent dédiés à la persistance et qu'on ait une couche dédiée à la logique métier qui soit découplée des points d'entrés.

Les usecases sont des fonctions qui ont accès au contexte et par lesquelles on va toujours passer pour déclencher de la logique depuis un point d’entrée. C'est à travers eux qu'on va manipuler les modèles et les services pour arriver à nos fins.

On essaye d’écrire des usecases qui ne fassent qu’une seule chose, dans l’idée de répondre aux besoins d’un point d’entrée en combinant plusieurs usecases (composition). Cela permet, à terme, d’avoir une grande flexibilité pour agencer les fonctionnalités existantes et répondre à de nouveaux besoins.

Conclusion

En se basant sur notre expérience et en suivant des préceptes de base : simplicité, tests, refacto continue, on a construit pas à pas une solution répondant spécifiquements aux besoins de ce projet. Notre client en est particulièrement satisfait et ne manque pas une occasion de nous vanter que c'est leur projet le plus stable, et celui sur lequel leur infra a le moins de travail.

Publié par

Louis Lebrault
Louis Lebrault

Self taught, Louis is now an experienced web developer. He discovered his passion for functional programming languages, especially Haskell.

Commentaires