Migrazione Express → Bun/Elysia
Portare un progetto Efesto Express esistente sullo stack Bun/Elysia
è in gran parte meccanico: la forma della classe, la convenzione per metodo (_<verb>,
_<verb>Swagger, _<verb>Multer), il DSL Swagger e il routing da filesystem si
trasferiscono invariati. Ciò che cambia davvero — firme degli handler, validazione,
bootstrap dell'app — è un residuo piccolo e concentrato.
Efesto include un codemod, efesto-migrate, che fa la parte meccanica al posto tuo e
segnala il resto. Il principio di design è ciò che lo rende affidabile.
Il principio di sicurezza
Il codemod si basa su un'asimmetria, non su una percentuale: può emettere codice che
non gira (un errore di compilazione, o un marker // EFESTO-TODO), ma mai codice
che gira e significa qualcosa di diverso dall'originale. Si applicano automaticamente solo
le trasformazioni provabilmente equivalenti; tutto il resto resta intatto e viene marcato
per la tua revisione.
Così non devi mai confrontare l'output riga per riga chiedendoti se un comportamento è cambiato in silenzio: se compila e non ci sono marker, la traduzione meccanica è fedele.
Come eseguirlo
Lavora su un git tree pulito così puoi rivedere il diff. Il codemod modifica i file in-place.
# dry-run: solo report, non scrive nulla
bunx efesto-migrate ./src/routes --dry
# applica le trasformazioni in-place
bunx efesto-migrate ./src/routes
Dentro il repo di Efesto puoi eseguirlo senza pubblicare:
bun packages/migrate/index.ts ./src/routes --dry
Il report elenca, per file, le trasformazioni automatiche applicate e ogni punto che
richiede conferma umana, ciascuno cercabile nel codice come commento // EFESTO-TODO:
efesto-migrate — 5 file analizzati
✔ 4 file con trasformazioni automatiche applicate (import, handler, return)
⚠ 3 punti richiedono conferma umana (cercati come // EFESTO-TODO):
src/routes/user/index.ts:12 [validation] _putValidation: converti la catena express-validator in TypeBox
src/routes/user/index.ts:24 [swaggerModel] `swaggerModel` è ignorato sullo stack Elysia: sposta gli schemi in efesto({ models })
src/routes/user/[id].ts:31 [cache] cache.key: identificatori fuori scope Elysia [allParams]
Cosa fa in automatico (AUTO)
Queste vengono applicate per te senza perdita di significato:
- Import —
from "efesto"→from "efesto/elysia". - Firma handler —
(req, res, next)→ destructuring dei soli campi usati, es.({ body, params }). Applicata solo quandoreqè usato esclusivamente comereq.{body,params,query,headers},ressolo dentroreturn res.json(x), enextnon è mai usato. - Accesso ai campi —
req.<campo>→<campo>, ereturn res.json(x)→return x. - Tipi generati — i riferimenti ai tipi generati (
*Types.*) vengono rimossi dalla firma dell'handler (li sostituiscono ilContextnativo / TypeBox).
Tutto ciò che è fuori da questa whitelist non viene riscritto, ma marcato.
Cosa segnala a te (// EFESTO-TODO)
Questi richiedono un umano perché non esiste una traduzione 1:1 fedele:
| Marker | Perché non può essere automatico |
|---|---|
| Handler non standard | res.status/res.send/res.redirect, next, req.file, o accessi non banali a req — il control flow va ripensato in (ctx) => return value. |
| express-validator → TypeBox | Un _<verb>Validation scritto come array (catena express-validator) deve diventare uno schema TypeBox { body, query, params }. Massimo rischio di drift semantico (coercion, optional, validator custom). |
swaggerModel | Lo swaggerModel a livello di service è ignorato sullo stack Elysia — solleva i suoi schemi nel registry centrale efesto({ models }). |
| Cache key non portabili | Template key/purgeKey che referenziano identificatori fuori dallo scope Elysia ({ctx, params, query, body, headers, store}) — es. req.user, variabili helper come allParams — non compilano e vanno riscritti. |
| import express | import da express / express-validator da rimuovere. |
Un caso sottile che il codemod intercetta: un handler che fa ...req.body e ha multer
su quel verbo non è un rename meccanico. Su Elysia il body unificato contiene anche i
file caricati, quindi spanderlo li farebbe trapelare — viene segnalato per conversione
manuale.
Riferimento del mapping
Se migri a mano, o vuoi capire un marker, ecco il mapping completo Express → Elysia. Ogni riga è classificata:
- MECCANICO — traduzione 1:1, gestita dal codemod.
- GIUDIZIO — richiede comprensione semantica; è il residuo vero.
- RISCRITTURA — nessun 1:1; riscritto una volta per progetto (bootstrap).
Definizione del service
Il target consigliato per un BaseApiService Express è la classe Elysia (non
l'istanza Elysia nativa) — preserva la convenzione _<verb> e minimizza il diff.
| Concetto | Express | Elysia | Tipo |
|---|---|---|---|
| Import | from "efesto" | from "efesto/elysia" | MECCANICO |
| Base class | extends BaseApiService | extends BaseApiService | IDENTICO |
| Costruttore | super(__filename) | opzionale, ignorato (path dedotto dal file) | MECCANICO |
| Convenzione verbi | _get, _getSwagger, _getValidation, _getMulter | identica | IDENTICO |
| Swagger DSL | _<verb>Swagger: SwaggerOptions (@Model, $ref, isPopulation…) | identico (stesso expandSwagger) | IDENTICO |
swaggerModel | sul service | ignorato → solleva in efesto({ models }) | RISCRITTURA |
Handler
| Express | Elysia | Tipo |
|---|---|---|
async _get(req, res, next) | _get(ctx: Context) | GIUDIZIO |
req.body / req.params / req.query | destrutturati dal Context: ({ body, params, query }) => | MECCANICO |
return res.json(x) | return x | MECCANICO |
res.status(n).json(x) | ({ set }) => { set.status = n; return x } | GIUDIZIO |
res.send / res.redirect / header manuali | API Elysia (set.headers, redirect, return raw) | GIUDIZIO |
next(err) / throw | throw (gestito da onError) | MECCANICO |
Tipi generati ModelXTypes.* | Context nativo (+ TypeBox) | GIUDIZIO |
Validazione
| Express | Elysia | Tipo |
|---|---|---|
_putValidation = [check(["name"]).exists()] (express-validator) | _putValidation: RouteValidation = { body: t.Object({ name: t.String() }) } (TypeBox) | GIUDIZIO |
Nessun 1:1: le catene express-validator diventano schemi TypeBox
({ body, query, params, response }). È il punto a più alto rischio di drift semantico.
Vedi Validazione.
Upload / Multer
| Express | Elysia | Tipo |
|---|---|---|
_putMulter = true / _putMultipleMulter = true | identici; senza un _<verb>Validation.body, Efesto inietta auto t.Object({ file: t.File() }) / t.Object({ files: t.Files() }) | IDENTICO |
req.file / req.files | body.file / body.files (web File) | MECCANICO |
| (precedenza) | dichiarare _<verb>Validation.body sovrascrive l'auto-body del multer — ri-dichiara lì files a mano | GIUDIZIO |
Vedi Upload di file.
Cache / purge
| Express | Elysia | Tipo |
|---|---|---|
config.redis: { host, port, defaultExpiresInSeconds } | efesto({ redis: {...} }) o redisClient | RISCRITTURA |
Chiavi cache/purgeKey statiche (es. `users`, `single-id-*`) | identiche (compilate 1 volta al mount) | MECCANICO |
Chiavi dinamiche req.params.id / req.query.x / req.body.x | params.id / query.x / body.x (scope: ctx, params, query, body, headers, store) | MECCANICO |
Chiavi che usano altri campi di req (req.user, req.tenantId…) | non in scope → riscrivi via store/ctx o ripensa la chiave | GIUDIZIO |
Vedi Caching.
Permessi / ABAC
| Express | Elysia | Tipo |
|---|---|---|
abacPermissions in config + req.ability via middleware | abac in efesto({ abac }) + ability via setup: app => app.derive(() => ({ ability })) | RISCRITTURA |
abacPermissions.reqAbilityField | abac.abilityField (default "ability") | MECCANICO |
abacPermissions.checkPermissionBeforeResolver | abac.checkPermissionBeforeResolver (default true) | MECCANICO |
shape permission { action, model } | permission: [action, model] | GIUDIZIO (shape diversa) |
Vedi Permessi ABAC.
_id → id, routing, runtime
| Express | Elysia | Tipo |
|---|---|---|
Efesto.mongoIdParser global | efesto({ mongoIdParser: true }) (off di default) | MECCANICO |
routes/user/[id].ts → /user/:id | identico | IDENTICO |
__dirname / __filename | import.meta.dir | MECCANICO |
Node + build tsc + node dist/... | Bun, niente build (bun run lib/server.ts) | RISCRITTURA |
Bootstrap app (riscrittura una-tantum)
L'entrypoint del server non ha mapping 1:1 — riscrivilo una volta, usando
example/elysia-project/lib/server.ts come template di riferimento.
Express (app.ts) | Elysia (server.ts) |
|---|---|
express() + bodyParser/cors/compression | efesto({...}).listen(port) (cors ecc. via setup o plugin Elysia) |
authMiddleware / errorMiddleware | setup: app => app.onBeforeHandle(...).onError(...) |
middleware req.ability = defineAbility(...) | setup: app => app.derive(() => ({ ability })) |
swagger via swagger-ui-express | @elysiajs/openapi nativo (servito a /<prefix>/openapi) |
Dopo il codemod
Il codemod gestisce la parte MECCANICA. Per arrivare a un runtime corretto, fai questi passi una-tantum:
- Risolvi i marker
// EFESTO-TODO— il residuo di GIUDIZIO (handler, express-validator → TypeBox, cache key non portabili). - Centralizza gli
swaggerModelinefesto({ models }), e aggiungi unonErrorche mappaerr.status(vedi il template di bootstrap sopra). - Type-check e verifica a runtime — Bun non fa type-check, quindi esegui
tsc --noEmit, poi avvia il server ed esercita le rotte migrate (cache, upload, errori).
tsc --noEmit contaBun esegue TypeScript senza type-check, quindi il codemod lascia di proposito errori di
compilazione loud (es. l'import di un tipo non esportato) accanto al marker
relativo. Eseguire tsc --noEmit li fa emergere: è il tuo vero gate di sicurezza, non il
runtime.
Vedi la guida allo stack Bun / Elysia per il modello di destinazione completo.