Move existing Node.js apps to MoroJS with predictable, mechanical rewrites. No magic, no rewriting your business logic — just the syntactic shifts that get you running on @morojs/moro.
Express is shipping today. The rest are next on the roadmap — open an issue or PR if you need one sooner.
Mechanical rewrites for the most common Express patterns. Battle-tested codemod with full test coverage.
View guideSchema-first migration with hook and plugin mapping to MoroJS modules and middleware.
Coming soonContext-style middleware translated into MoroJS request/response handlers.
Coming soonDecorator-driven controllers and providers mapped onto MoroJS modules and dependency injection.
Coming soonThe Express codemod walks your project tree and rewrites Express imports plus the usual factory and middleware helpers to their MoroJS equivalents. It is intentionally small and predictable — you keep ownership of the rest.
See exactly what will change before touching a single file.
# Clone the migrations repo
git clone https://github.com/Moro-JS/migrations.git
cd migrations
# Dry-run against your project (prints a colored diff, no writes)
node express/scripts/migrate-express.mjs /path/to/your/appAdd --write to commit the changes in place.
# Apply changes in place
node express/scripts/migrate-express.mjs /path/to/your/app --write
# Limit which extensions are scanned (default: ts,tsx,js,mjs,cjs)
node express/scripts/migrate-express.mjs /path/to/your/app --write --ext=ts,jsOnce the diff looks good, install MoroJS and remove Express. The rewritten imports already point at @morojs/moro.
# Install MoroJS in your project
npm install @morojs/moro
# Remove Express once the migration is verified
npm uninstall expressA representative file the codemod will rewrite for you.
1// Express
2const express = require('express');
3const app = express();
4
5app.use(express.json());
6app.use(express.urlencoded({ extended: true }));
7app.use('/public', express.static('./public'));
8
9const router = express.Router();
10router.get('/users/:id', (req, res) => {
11 res.json({ id: req.params.id });
12});
13
14app.use('/api', router);
15app.listen(3000);1// MoroJS
2import { createApp, createRouter, json, urlencoded, middleware } from '@morojs/moro';
3const app = await createApp(); // createApp() is async — await it.
4
5app.use(json());
6app.use(urlencoded({ extended: true }));
7app.use(middleware.staticFiles({ root: './public', prefix: '/public' }));
8
9const router = createRouter();
10router.get('/users/:id', (req, res) => {
11 res.json({ id: req.params.id });
12});
13
14app.use('/api', router);
15app.listen(3000);The full set of mechanical swaps the codemod performs.
| Express | MoroJS |
|---|---|
require('express') | named imports from '@morojs/moro' |
import ... 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)) | middleware.staticFiles({ root: dir, prefix: '/p' }) |
Request / Response (types) | HttpRequest / HttpResponse |
<dir>The root of the codebase to scan. Required positional argument.
--writeApply the changes in place. Without this flag the script runs as a dry run.
--ext=ts,jsComma-separated extension filter. Defaults to ts,tsx,js,mjs,cjs.
The script automatically skips node_modules, dist, build, .git, .next, and coverage.
These have no mechanical equivalent. The codemod prints a Needs manual work report with file and line rather than emitting code that looks right and silently misbehaves.
app.use('/prefix', middlewareFn) — MoroJS only honours a path prefix when argument 2 is a createRouter() instance. Anything else is a silent no-op — the middleware simply never runs. express.static() at a prefix is the exception: it is rewritten into staticFiles’ prefix option.express.static(dir, { maxAge }) — Express maxAge is milliseconds; MoroJS writes the value straight into Cache-Control: max-age=, which is seconds.app.set() / app.locals / app.engine() — No equivalent. Use createApp(options) or moro.config.js, and app.decorate() / res.locals.Top-level await in CommonJS — createApp() is async and top-level await is ESM-only. Wrap the module body in an async IIFE or convert the file to ESM.app.listen() callback style, 4-arg error middleware, next(err), and the res.status / res.json / res.send surface.import { Router as R } from 'express' have their source updated, but local names stay as you wrote them.You are now on MoroJS. Here is where to go next to take advantage of the things Express never gave you.
Replace ad-hoc route files with chainable, type-safe routes that infer params, queries, and bodies.
Read the docsBring Zod, Joi, Yup, or class-validator into the request lifecycle without writing custom middleware.
Read the docsTake the same MoroJS app to Node, Lambda, Cloudflare Workers, or Vercel Edge with no rewrites.
Read the docsThe migrations repo is intentionally small and easy to read. Open an issue with the patterns you want automated, or contribute a codemod for your stack.