My App

Architecture modulaire

router → service → repository, un fichier = une responsabilité

Chaque module suit le pattern router → service. Le router valide et route, le service contient la logique métier, le repository gère l'accès aux données.

Structure d'un module

packages/api/src/
  routers/
    establishment.router.ts    ← Définition des procédures tRPC
  services/
    establishment.service.ts   ← Logique métier
  schemas/
    establishment.schema.ts    ← Validation Zod

packages/db/src/
  schema/
    establishment.ts           ← Schéma Drizzle

Responsabilités

Router (.router.ts)

  • Définit les procédures tRPC (query, mutation)
  • Applique les middlewares (auth, rôles)
  • Valide les inputs avec un schéma Zod
  • Délègue au service — aucune logique métier
// ✅ Bon
export const establishmentRouter = router({
  create: adminProcedure
    .input(createEstablishmentSchema)
    .mutation(({ input, ctx }) => {
      return establishmentService.create(input, ctx.session.user);
    }),
});

// ❌ Mauvais — logique métier dans le router
export const establishmentRouter = router({
  create: adminProcedure
    .input(createEstablishmentSchema)
    .mutation(async ({ input, ctx }) => {
      const est = await db.insert(establishment).values(input);
      await sendMagicLink(est.email);
      return est;
    }),
});

Service (.service.ts)

  • Contient la logique métier
  • Orchestre les appels DB et les effets de bord (emails, notifications)
  • Gère les erreurs métier
// establishment.service.ts
export const establishmentService = {
  async create(input: CreateEstablishment, user: User) {
    const est = await db.insert(establishment).values(input).returning();

    // Créer le compte pour l'établissement
    const adminUser = await createEstablishmentAdmin(est[0]);

    // Envoyer le magic link
    await auth.api.sendMagicLink({ email: adminUser.email });

    // Créer les issues prédéfinies par défaut
    await seedDefaultIssues(est[0].id);

    return est[0];
  },
};

Schema (.schema.ts)

  • Validation Zod des inputs
  • Utilisé par le router ET potentiellement par le frontend (formulaires)
// establishment.schema.ts
export const createEstablishmentSchema = z.object({
  name: z.string().min(1).max(255),
  type: z.enum(["hotel", "restaurant", "company", "agency", "other"]),
  address: z.string().min(1).max(500),
  // ...
});

export type CreateEstablishment = z.infer<typeof createEstablishmentSchema>;

Règles

  1. Un fichier = une responsabilité — pas de fichier "utils" fourre-tout
  2. Le router ne contient pas de logique — il valide et délègue
  3. Le service ne touche pas à la requête HTTP — il reçoit des données typées
  4. Les schémas sont partagés — le même schéma Zod valide côté client et serveur
  5. Pas d'imports circulaires — les dépendances vont dans un seul sens : router → service → db

On this page