Passa al contenuto principale

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:

  • Importfrom "efesto"from "efesto/elysia".
  • Firma handler(req, res, next) → destructuring dei soli campi usati, es. ({ body, params }). Applicata solo quando req è usato esclusivamente come req.{body,params,query,headers}, res solo dentro return res.json(x), e next non è mai usato.
  • Accesso ai campireq.<campo><campo>, e return res.json(x)return x.
  • Tipi generati — i riferimenti ai tipi generati (*Types.*) vengono rimossi dalla firma dell'handler (li sostituiscono il Context nativo / 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:

MarkerPerché non può essere automatico
Handler non standardres.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 → TypeBoxUn _<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).
swaggerModelLo swaggerModel a livello di service è ignorato sullo stack Elysia — solleva i suoi schemi nel registry centrale efesto({ models }).
Cache key non portabiliTemplate 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 expressimport 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.

ConcettoExpressElysiaTipo
Importfrom "efesto"from "efesto/elysia"MECCANICO
Base classextends BaseApiServiceextends BaseApiServiceIDENTICO
Costruttoresuper(__filename)opzionale, ignorato (path dedotto dal file)MECCANICO
Convenzione verbi_get, _getSwagger, _getValidation, _getMulteridenticaIDENTICO
Swagger DSL_<verb>Swagger: SwaggerOptions (@Model, $ref, isPopulation…)identico (stesso expandSwagger)IDENTICO
swaggerModelsul serviceignorato → solleva in efesto({ models })RISCRITTURA

Handler

ExpressElysiaTipo
async _get(req, res, next)_get(ctx: Context)GIUDIZIO
req.body / req.params / req.querydestrutturati dal Context: ({ body, params, query }) =>MECCANICO
return res.json(x)return xMECCANICO
res.status(n).json(x)({ set }) => { set.status = n; return x }GIUDIZIO
res.send / res.redirect / header manualiAPI Elysia (set.headers, redirect, return raw)GIUDIZIO
next(err) / throwthrow (gestito da onError)MECCANICO
Tipi generati ModelXTypes.*Context nativo (+ TypeBox)GIUDIZIO

Validazione

ExpressElysiaTipo
_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

ExpressElysiaTipo
_putMulter = true / _putMultipleMulter = trueidentici; senza un _<verb>Validation.body, Efesto inietta auto t.Object({ file: t.File() }) / t.Object({ files: t.Files() })IDENTICO
req.file / req.filesbody.file / body.files (web File)MECCANICO
(precedenza)dichiarare _<verb>Validation.body sovrascrive l'auto-body del multer — ri-dichiara lì files a manoGIUDIZIO

Vedi Upload di file.

Cache / purge

ExpressElysiaTipo
config.redis: { host, port, defaultExpiresInSeconds }efesto({ redis: {...} }) o redisClientRISCRITTURA
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.xparams.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 chiaveGIUDIZIO

Vedi Caching.

Permessi / ABAC

ExpressElysiaTipo
abacPermissions in config + req.ability via middlewareabac in efesto({ abac }) + ability via setup: app => app.derive(() => ({ ability }))RISCRITTURA
abacPermissions.reqAbilityFieldabac.abilityField (default "ability")MECCANICO
abacPermissions.checkPermissionBeforeResolverabac.checkPermissionBeforeResolver (default true)MECCANICO
shape permission { action, model }permission: [action, model]GIUDIZIO (shape diversa)

Vedi Permessi ABAC.

_idid, routing, runtime

ExpressElysiaTipo
Efesto.mongoIdParser globalefesto({ mongoIdParser: true }) (off di default)MECCANICO
routes/user/[id].ts/user/:ididenticoIDENTICO
__dirname / __filenameimport.meta.dirMECCANICO
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/compressionefesto({...}).listen(port) (cors ecc. via setup o plugin Elysia)
authMiddleware / errorMiddlewaresetup: 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:

  1. Risolvi i marker // EFESTO-TODO — il residuo di GIUDIZIO (handler, express-validator → TypeBox, cache key non portabili).
  2. Centralizza gli swaggerModel in efesto({ models }), e aggiungi un onError che mappa err.status (vedi il template di bootstrap sopra).
  3. 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).
Perché tsc --noEmit conta

Bun 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.