Elysia
Comment Vantablvck exploite l'écosystème Elysia, des macros de sécurité à l'OpenAPI généré.
Le point d'avoir choisi Elysia plutôt qu'un routeur minimal, c'est d'exploiter son écosystème. Elysia n'est pas utilisé comme un Express typé, il est utilisé pour ce qu'il fait de mieux : encoder les garde-fous dans le pipeline du framework.
Le serveur est encore un squelette. Cette page décrit le design cible et les patterns idiomatiques que le backend suivra module par module.
Un module, une instance
Chaque feature est une instance Elysia, composée à la racine via .use(). Les services sont des classes découplées du framework, les schémas de validation sont exportés à part. Une route délègue au service, le service ne touche pas au framework.
apps/server/src/
modules/
posts/
index.ts # instance Elysia, les routes
service.ts # logique métier, découplée
model.ts # schémas Zod partagés
plugins/
safety.ts # les macros de sécurité
index.ts # composition racineLes macros encodent la sécurité
C'est le vrai levier. Les garde-fous (authentification, niveau de confiance, limitation de débit) sont des macros, déclaratives et typées sur chaque route. La friction anti-abus et la proportionnalité vivent donc au niveau du framework, pas dispersées dans les handlers.
// apps/server/src/plugins/safety.ts
export const safety = new Elysia({ name: "safety" }).macro({
auth: {
resolve: async ({ request, status }) => {
const session = await auth.api.getSession({ headers: request.headers });
if (!session) return status(401, "Sign in required");
return { user: session.user };
},
},
requireTrust: (min: "new" | "active" | "trusted") => ({
resolve: ({ user, status }) =>
trustRank(user.trustLevel) < trustRank(min)
? status(403, "Not allowed yet")
: {},
}),
rateLimit: (bucket: string) => ({
beforeHandle: ({ user, status }) =>
limiter.ok(bucket, user.id) ? undefined : status(429, "Slow down"),
}),
});Sur une route, tout devient déclaratif :
// apps/server/src/modules/posts/index.ts
export const posts = new Elysia({ prefix: "/posts" })
.use(safety)
.post("/", ({ user, body }) => PostService.create(user.id, body), {
auth: true,
requireTrust: "active", // les nouveaux comptes sont bridés
rateLimit: "post:create",
body: CreatePost, // schéma Zod partagé
});Un post qui exige d'être connecté, active au minimum, rate-limité, et validé. Le tout typé de bout en bout jusqu'au client Eden.
Validation avec Zod
Elysia supporte les Standard Schema, donc Zod se branche directement dans body, query et params. La règle du projet reste tenue : validation Zod sur toute entrée. Un seul schéma valide au runtime, produit le type, et alimente l'OpenAPI.
import { z } from "zod";
const CreatePost = z.object({
body: z.string().max(500),
replyToId: z.string().ulid().optional(),
});Ce qu'on branche de l'écosystème
| Plugin ou capacité | Usage |
|---|---|
@elysiajs/openapi | Documentation d'API générée depuis les schémas, front Scalar. Les routes de modération sont exclues de la doc publique |
@elysiajs/opentelemetry | Observabilité dès le début, un garde-fou |
| WebSocket natif | Temps réel : notifications, puis messages privés |
t.File avec fileType | Upload sécurisé : le vrai type est validé par magic number, pas seulement par le MIME |
| Eden Treaty | Types partagés du serveur vers les clients web et native |
Le lifecycle au service de la safety
Elysia découpe une requête en étapes. On les exploite pour la sécurité, pas seulement pour router.
| Étape | Usage |
|---|---|
onRequest | Limitation de débit globale |
resolve | Injection de l'utilisateur validé |
onAfterResponse | Écriture du journal d'audit, déclenchement du calcul d'acharnement en asynchrone, sans bloquer la réponse |
onError | Erreurs typées et messages clairs |
Upload de média sécurisé
L'upload passe par t.File avec fileType, qui vérifie le vrai type du fichier par sa signature binaire. Cela sert directement le garde-fou du média : un fichier déguisé est rejeté, et rien n'est diffusé avant le scan complet.