My App
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_admin

Conventions

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

CodeUsage
UNAUTHORIZEDSession absente ou expirée
FORBIDDENRôle insuffisant
NOT_FOUNDRessource introuvable
BAD_REQUESTInput invalide (après Zod)
TOO_MANY_REQUESTSRate limit atteint (anti-spam)
CONFLICTDoublon (email, nom unique, etc.)

On this page