API
Design d'API
Conventions tRPC, routes publiques, validation, organisation
L'API est le contrat entre le frontend et le backend. Tout passe par tRPC pour les routes authentifiées, et par Elysia directement pour les routes publiques (page de signalement).
Architecture API
/trpc/* → tRPC (authentifié, type-safe)
/api/auth/* → Better Auth (auth endpoints)
/report/:id/:token → Elysia (public, page de signalement)Organisation des routers tRPC
packages/api/src/
index.ts → t.router, publicProcedure, protectedProcedure
context.ts → createContext (session via Better Auth)
routers/
index.ts → appRouter (agrège tous les routers)
establishment.router.ts → CRUD établissements
toilet.router.ts → CRUD WC
qrcode.router.ts → Gestion QR codes
report.router.ts → Signalements (vue dashboard)
notification.router.ts → Config notifications
admin.router.ts → Routes super_adminConventions
1. Procédures protégées par rôle
// Middleware de vérification de rôle
const adminProcedure = protectedProcedure.use(({ ctx, next }) => {
if (ctx.session.user.role !== "super_admin") {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({ ctx });
});
const establishmentProcedure = protectedProcedure.use(({ ctx, next }) => {
if (!["establishment_admin", "super_admin"].includes(ctx.session.user.role)) {
throw new TRPCError({ code: "FORBIDDEN" });
}
return next({ ctx });
});2. Validation Zod systématique
// ✅ Bon — schema Zod sur chaque input
establishment.create = adminProcedure
.input(createEstablishmentSchema)
.mutation(({ input, ctx }) => { ... });
// ❌ Mauvais — pas de validation
establishment.create = adminProcedure
.mutation(({ input }) => { ... });3. Services séparés des routers
// ✅ Bon — le router délègue au service
.mutation(({ input, ctx }) => {
return establishmentService.create(input, ctx.session.user);
})
// ❌ Mauvais — logique métier dans le router
.mutation(({ input, ctx }) => {
const result = await db.insert(establishment).values(input);
await sendEmail(...);
return result;
})Routes publiques (Elysia)
La page de signalement est une route publique (pas d'auth) gérée directement par Elysia :
// apps/server/src/routes/report.ts
app.get("/report/:toiletId/:token", async ({ params }) => {
// 1. Vérifier que le token est valide et non expiré
// 2. Vérifier rate limit (IP, fingerprint)
// 3. Retourner les données du WC + problèmes prédéfinis
});
app.post("/report/:toiletId/:token", async ({ params, body }) => {
// 1. Anti-spam checks
// 2. Créer le signalement
// 3. Déclencher les notifications
});Schémas de validation (Zod)
// packages/api/src/schemas/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),
city: z.string().min(1).max(255),
postalCode: z.string().min(1).max(20),
country: z.string().length(2).default("FR"),
phone: z.string().max(20).optional(),
email: z.email(),
whatsappNumber: z.string().max(20).optional(),
});
// packages/api/src/schemas/report.schema.ts
export const createReportSchema = z.object({
category: z.enum(["cleanliness", "consumables", "equipment", "other"]),
predefinedIssueId: z.string().uuid().optional(),
freeText: z.string().max(200).optional(),
});Codes d'erreur
| Code | Usage |
|---|---|
UNAUTHORIZED | Session absente ou expirée |
FORBIDDEN | Rôle insuffisant |
NOT_FOUND | Ressource introuvable |
BAD_REQUEST | Input invalide (après Zod) |
TOO_MANY_REQUESTS | Rate limit atteint (anti-spam) |
CONFLICT | Doublon (email, nom unique, etc.) |