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