Small tooling to move existing Express code toward MoroJS (@morojs/moro) with predictable, mechanical rewrites.
Verified against @morojs/moro 1.8.3.
| Path | Purpose |
|---|---|
express/scripts/migrate-express.mjs |
Walks a directory tree, rewrites Express imports and the usual factory/middleware helpers to MoroJS equivalents, and reports what it could not translate. |
From the repo root (or anywhere), run Node against the script and pass the root of the codebase you want to scan:
node express/scripts/migrate-express.mjs /path/to/your/app # dry-run: prints diffs, no writes
node express/scripts/migrate-express.mjs /path/to/your/app --write # apply changes in placeOptional:
--ext=ts,js— comma-separated extensions (default:ts,tsx,js,mjs,cjs)
The script skips node_modules, dist, build, .git, .next, and coverage.
| Express | MoroJS |
|---|---|
require('express') / from 'express' |
named imports from @morojs/moro |
express() |
await createApp() |
express.Router() |
createRouter() |
express.json(...) |
json(...) |
express.urlencoded(...) |
urlencoded(...) |
express.static(dir) |
middleware.staticFiles({ root: dir }) |
app.use('/p', express.static(dir)) |
app.use(middleware.staticFiles({ root: dir, prefix: '/p' })) — needs @morojs/moro >= 1.8.8 |
Request / Response (types) |
HttpRequest / HttpResponse (aliased on import) |
This is the part that has to be right or the output does not run:
createApp()isasync. It returnsPromise<Moro>, so it must be awaited — otherwiseappis a Promise and everyapp.use/app.get/app.listenthrows.@morojs/morohas no default export. Leavingimport express from '@morojs/moro'in place is a hard ESM error, so the default binding is dropped.- The names the rewrite introduces have to be bound.
createApp,createRouter,json,urlencoded, andmiddlewareare collected per file and emitted as a single import/require.
// before
const express = require('express');
const app = express();
app.use(express.json());
const router = express.Router();
// after
const { middleware, createApp, createRouter, json } = require('@morojs/moro');
const app = await createApp();
app.use(json());
const router = createRouter();These have no mechanical equivalent in MoroJS 1.8.3. The script prints a “Needs manual work” report with file and line rather than emitting code that looks right and silently misbehaves.
| Pattern | Why |
|---|---|
app.use('/prefix', middlewareFn) |
MoroJS only honours a path prefix when argument 2 is a createRouter() instance. Anything else is a silent no-op — no error, no warning, the middleware simply never runs. (express.static() at a prefix is the one exception: it is rewritten, into staticFiles' prefix option.) |
express.static(dir, { maxAge }) |
Express’s maxAge is milliseconds; MoroJS writes the value straight into Cache-Control: max-age=, which is seconds. |
Wildcard route paths ('/files/*') |
MoroJS’s path matcher never compiles * to a wildcard — it survives into the regex as a quantifier, so /files/* matches /files/ but not /files/a.txt. |
app.set() / app.locals / app.engine() |
No equivalent. Use createApp(options) or moro.config.js, and app.decorate() / res.locals. |
res.render() |
Only exists once middleware.template({...}) is installed, and MoroJS ships its own minimal template syntax — it does not run Pug/EJS/Handlebars. |
NextFunction |
Not exported. MoroJS types next as () => void; see the Middleware type. |
Top-level await createApp() in CommonJS |
Top-level await is ESM-only. Wrap the module body in an async IIFE or convert the file to ESM. |
Already compatible, nothing to rewrite: app.listen() callback style, 4-arg error middleware, next(err), and the res.status / res.json / res.send / res.cookie / res.redirect surface.
The codemod only performs the swaps in the table above. It does not rewrite arbitrary middleware or routing APIs. Aliases like import { Router as R } from 'express' get the import source updated; local names stay as you wrote them. Always dry-run and review the diff first, especially in projects with custom wrappers around Express.
npm install
npm testexpress/tests/migrate-express.test.ts runs the script as a subprocess against temp fixtures and asserts against the real @morojs/moro 1.8.3 API surface.