Clean code et refactoring - 17 - Structurer un projet — feature-based vs layer-based

Layer-based ou feature-based ? Le choix de la structure de dossiers a un impact direct sur la maintenabilité du projet.

  1. 01 Clean code et refactoring - 00 - Pourquoi le clean code est un investissement, pas un luxe
  2. 02 Clean code et refactoring - 01 - Nommage : la competence la plus sous-estimee
  3. 03 Clean code et refactoring - 02 - Fonctions : courtes, claires, responsables
  4. 04 Clean code et refactoring - 03 - Conditions et lisibilité : sortir de la pyramide
  5. 05 Clean code et refactoring - 04 - Commentaires et documentation : quand le code ne suffit pas
  6. 06 Clean code et refactoring - 05 - Immutabilite et effets de bord : moins de surprises, moins de bugs
  7. 07 Clean code et refactoring - 06 - Gestion des erreurs propre : fail fast, fail loud
  8. 08 Clean code et refactoring - 07 - Programmation defensive vs offensive : valider aux frontieres, faire confiance a l'intérieur
  9. 09 Clean code et refactoring - 08 - SOLID en pratique avec TypeScript
  10. 10 Clean code et refactoring - 09 - DRY, KISS, YAGNI
  11. 11 Clean code et refactoring - 10 - Couplage et cohesion
  12. 12 Clean code et refactoring - 11 - Complexite cyclomatique
  13. 13 Clean code et refactoring - 12 - Abstractions prematurees vs tardives
  14. 14 Clean code et refactoring - 13 - Code smells
  15. 15 Clean code et refactoring - 14 - Techniques de refactoring
  16. 16 Clean code et refactoring - 15 - Refactoring legacy sans tout casser
  17. 17 Clean code et refactoring - 16 - Tests comme filet de sécurité pour le refactoring
  18. 18 Clean code et refactoring - 17 - Structurer un projet — feature-based vs layer-based
  19. 19 Clean code et refactoring - 18 - Constantes, configuration et magic numbers
  20. 20 Clean code et refactoring - 19 - Linting et formatting — ESLint, Biome, automatiser la qualité
  21. 21 Clean code et refactoring - 20 - Conventions d'équipe et ADR
  22. 22 Clean code et refactoring - 21 - Dette technique — quand elle est acceptable, quand elle tue le projet
  23. 23 Clean code et refactoring - 22 - Code review — donner et recevoir du feedback
  24. 24 Clean code et refactoring - 23 - Glossaire — tous les termes de la serie

17 - Structurer un projet — feature-based vs layer-based

Ce que tu vas apprendre

  • La différence entre structure par couches et structure par feature
  • Pourquoi le dossier utils/ est un piège
  • Quand créer un nouveau dossier
  • Les conventions de nommage de fichiers qui marchent
  • La colocation : tests, styles et composants au meme endroit
  • Un exemple concret de structure de projet

Prerequisites

Article précédent : 16 - Tests et refactoring


Sur mon premier projet professionnel, la structure du backend ressemblait a ca : controllers/, services/, models/, utils/, helpers/, middlewares/. Classique. Au bout de six mois et 200 fichiers, trouver le code de la fonctionnalité "panier" demandait d'ouvrir cinq dossiers différents. Le fichier utils/index.ts faisait 1 800 lignes. Personne ne savait plus ou mettre quoi.

La structure de ton projet est une décision d'architecture. Elle conditionne la facon dont les gens naviguent dans le code, comprennent les limites des fonctionnalités et identifient les dépendances.

Layer-based : l'approche classique

La structure par couches organise le code par rôle technique :

src/
  controllers/
    userController.ts
    orderController.ts
    productController.ts
  services/
    userService.ts
    orderService.ts
    productService.ts
  models/
    User.ts
    Order.ts
    Product.ts
  repositories/
    userRepository.ts
    orderRepository.ts
    productRepository.ts

Ca marche pour les petits projets. Dix fichiers par dossier, c'est genable. Mais ca scale mal. Quand tu ajoutes des features, chaque dossier grossit. Les fichiers lies a une meme feature sont eparpilles partout. Pour modifier la logique des commandes, tu ouvres quatre dossiers.

L'autre problème : les dépendances entre features deviennent invisibles. orderService.ts importe-t-il userService.ts ? Tu ne le vois pas dans l'arborescence. Il faut ouvrir le fichier pour le savoir.

Feature-based : organiser par domaine

La structure par feature regroupe tout ce qui concerne un meme domaine :

src/
  users/
    userController.ts
    userService.ts
    userRepository.ts
    User.ts
    user.test.ts
    user.routes.ts
  orders/
    orderController.ts
    orderService.ts
    orderRepository.ts
    Order.ts
    order.test.ts
    order.routes.ts
  shared/
    database.ts
    logger.ts
    auth.middleware.ts

Les avantages sont concrets. Tu veux comprendre la feature "orders" ? Tout est dans un seul dossier. Tu veux supprimer une feature ? Tu supprimes le dossier. Les dépendances entre features deviennent explicites : si orders/ importe depuis users/, c'est visible dans les imports.

Pour un projet React ou Next.js, le principe est le meme :

src/
  features/
    auth/
      LoginForm.tsx
      LoginForm.test.tsx
      useAuth.ts
      authApi.ts
    dashboard/
      DashboardPage.tsx
      DashboardPage.test.tsx
      StatCard.tsx
      useDashboardData.ts

Le piège du dossier utils/

Le dossier utils/ est une poubelle deguisee en organisation. J'en ai vu des dizaines. Ils contiennent tous la meme chose : des fonctions qui n'ont aucun rapport entre elles. formatDate, deepClone, parseQueryString, retryWithBackoff, calculateTax — tout dans le meme fichier, parce que personne ne savait ou les mettre.

Le problème n'est pas d'avoir des fonctions utilitaires. Le problème est de les regrouper dans un fourre-tout. La solution : coloquer les fonctions avec le code qui les utilise.

typescript// Avant : utils/format.ts contient 40 fonctions
// Apres : chaque feature a ses propres fonctions
// orders/formatOrderDate.ts
export function formatOrderDate(date: Date): string {
  return new Intl.DateTimeFormat("fr-FR", {
    day: "numeric",
    month: "long",
    year: "numeric",
  }).format(date);
}

Si une fonction est vraiment partagee entre plusieurs features (comme un logger ou un client HTTP), elle va dans shared/. Pas dans utils/. Le nom shared/ rend explicite que ce code est une dépendance transversale.

Quand créer un nouveau dossier

La regle que j'utilise : un dossier a du sens à partir de trois fichiers lies. Un seul fichier dans un dossier, c'est de la sur-organisation. Deux, c'est debattable. Trois, c'est justifie.

Il y a aussi la question de la profondeur. Plus de trois niveaux d'imbrication rend la navigation penible. src/features/orders/components/forms/fields/QuantityInput.tsx est trop profond. src/features/orders/QuantityInput.tsx est suffisant tant que le dossier n'a pas trente fichiers.

Conventions de nommage de fichiers

Les conventions les plus courantes en TypeScript/JavaScript :

Convention Exemple Usage
camelCase userService.ts Services, utilitaires
PascalCase UserCard.tsx Composants React
kebab-case user-service.ts Style Angular, fichiers config

Choisis une convention et respecte-la dans tout le projet. Le pire, c'est le melange. Si tu veux approfondir les conventions d'équipe, l'article 20 - Conventions et ADR traite le sujet en détail. Tu trouveras aussi des reflexions sur ce type de choix techniques sur paltemps.fr.

Les fichiers index : avec moderation

Les fichiers index.ts servent a simplifier les imports :

typescript// features/orders/index.ts
export { OrderService } from "./orderService";
export { Order } from "./Order";
export { orderRoutes } from "./order.routes";

// Ailleurs dans le code
import { OrderService, Order } from "@/features/orders";

C'est pratique pour les consommateurs externes du module. Mais il y a un piège : les barrel files massifs qui reexportent des centaines de symboles ralentissent le bundler et creent des dépendances circulaires. Utilise-les uniquement pour l'API publique d'une feature. Les imports internes a la feature restent directs.

Colocation : tout au meme endroit

La colocation est un principe simple : place les fichiers qui changent ensemble au meme endroit. Tests a cote du code. Styles a cote des composants. Types a cote de l'implementation.

features/
  orders/
    OrderList.tsx
    OrderList.test.tsx
    OrderList.module.css
    orderTypes.ts
    orderApi.ts
    orderApi.test.ts

C'est l'oppose de l'approche ou tous les tests sont dans un dossier __tests__/ a la racine et tous les styles dans un dossier styles/. Cette approche séparée oblige a maintenir une structure miroir qui diverge inevitablement du code source.

Exemple concret : structure d'une API Node.js

Voici une structure que j'utilise en production pour une API de taille moyenne (20-30 endpoints) :

src/
  features/
    auth/
      auth.controller.ts
      auth.service.ts
      auth.routes.ts
      auth.test.ts
      auth.types.ts
    users/
      users.controller.ts
      users.service.ts
      users.repository.ts
      users.routes.ts
      users.test.ts
      users.types.ts
    orders/
      orders.controller.ts
      orders.service.ts
      orders.repository.ts
      orders.routes.ts
      orders.test.ts
      orders.types.ts
  shared/
    database/
      client.ts
      migrations/
    middleware/
      auth.middleware.ts
      error.middleware.ts
      validation.middleware.ts
    logger.ts
    config.ts
  app.ts
  server.ts

Chaque feature est autonome. Les dépendances entre features passent par les exports publics. Le dossier shared/ contient uniquement le code transversal. C'est lisible, navigable, et ca supporte la croissance du projet.

Résumé

  • La structure par couches (layer-based) fonctionne pour les petits projets mais scale mal
  • La structure par feature regroupe le code par domaine et rend les dépendances explicites
  • Le dossier utils/ est un piège : préféré la colocation ou un dossier shared/
  • Cree un dossier à partir de trois fichiers lies, pas avant
  • Choisis une convention de nommage de fichiers et respecte-la partout
  • Les fichiers index sont utiles en moderation, dangereux en exces
  • La colocation (tests, styles, types a cote du code) facilite la maintenance

Article précédent : 16 - Tests et refactoring

Article suivant : 18 - Constantes et configuration

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.