Sixty seconds from now, your API is running.
One command scaffolds a typed, validated, production-ready backend. Auth, docs, WebSockets if you want them, and Moro's native engine already inside. There's nothing left to wire together before you write your first route.
The whole first minute, nothing skipped.
➜ npx @morojs/cli init my-api ✔ template · api ✔ validation · zod ✔ features · auth, docs ✔ project scaffolded ✔ dependencies installed next: cd my-api && npm run dev
--fast and skip them all.my-api/
├── moro.config.ts
└── src/
├── app.ts
├── routes/ ← your endpoints
├── modules/ ← versioned features
├── middleware/
├── validation/
└── types/➜ npm run dev moro v1.8.3 · engine moro (native) ✔ auth ready ✔ openapi docs at /docs listening on http://localhost:3000
➜ curl localhost:3000/health { "status": "ok", "engine": "moro" } ➜ open http://localhost:3000/docs # your API, documented, before you wrote a line
Every Node framework makes you choose
Picking a framework mostly means picking which compromise you can live with. There are production apps sitting in every corner of this chart.
The parts you always build are already built
Auth, validation, real-time, middleware ordering. The things every project ends up rebuilding are a few declared lines here. Each tab below is a whole file, not a snippet.
Schema-first routing. Validate, type, and handle a route in one chain, without separate middleware files, DTO classes or decorators.
import { createApp, z } from '@morojs/moro'; const app = await createApp(); app.post('/users') .body(z.object({ name: z.string().min(1), email: z.string().email(), age: z.number().int().min(18), })) .handler((req) => { // req.body is typed and already validated return { id: crypto.randomUUID(), ...req.body }; }); app.listen(3000);
The schema is the type
req.body and req.query are fully typed from the schema. No interfaces to keep in sync.
Validation before your code
Bad requests get a 400 with structured field errors before the handler ever runs.
Bring your validator
Zod, Joi, Yup, or Class Validator. Swap without rewriting routes.
Middleware that orders itself. Declare rate limits, validation, and caching in any order. Moro sorts them into ten fixed execution phases, every time.
const app = await createApp({ cors: true, helmet: true }); // add these in any order you likeapp.post('/auth/login') .body(z.object({ email: z.string().email(), password: z.string().min(8), })) .rateLimit({ requests: 5, window: 60_000 }) .cache({ ttl: 0 }) .handler(login); // declared after body and cache, still runs as// security → parsing → rate limit → validation → handler
Ten fixed phases
Security, parsing, rate limit, auth, validation, then your handler. Moro applies that order however you declared them.
Reordering is safe
Move the calls around in the chain and the execution order stays exactly the same.
Built-ins included
cors, helmet, compression, body parsing. Nothing extra to install.
Real-time, in the same app. Sockets share your API's auth, sessions and middleware, so there's no second server to run and no plugin to go find.
app.websocket('/chat', { 'join-room': (socket, { room, username }) => { socket.data.username = username; socket.join(room); socket.to(room).emit('user-joined', { username }); }, 'send-message': (socket, { room, message }) => { socket.to(room).emit('new-message', { user: socket.data.username, message, }); },});
One app instance
WebSockets share the auth, sessions, and middleware of your HTTP routes.
Pluggable transports
Socket.IO, ws, or uWebSockets. Pick what fits, keep the same code.
Pick your protocol
WebSockets, SSE, GraphQL subscriptions, gRPC streaming. Same app.
Auth without glue code. OAuth providers, role-based access, JWT or database sessions are all part of the framework already.
import { createApp, auth, providers } from '@morojs/moro'; const app = await createApp(); app.use(auth({ secret: process.env.AUTH_SECRET!, providers: [ providers.github({ ... }), providers.google({ ... }), ], session: { strategy: 'jwt' },})); // github, google, microsoft, discord, apple, custom oidc
Production ready
Email and password, magic links, 2FA, organization roles.
No glue code
No passport, no next-auth, no separate auth server to run.
Provider agnostic
GitHub, Google, Microsoft, Discord, Apple, or any custom OIDC.
Every new project boots on the fast engine
It's Moro's own, and it falls back to plain Node automatically on hosts where it can't load. There's no flag to set and no tuning step. The numbers below are the same hello-world app measured with wrk, 100 connections, 40 seconds, on an Apple M-series box.
July 2026 run. Expect different absolute numbers on your own hardware. Full methodology →
➜ wrk -c100 -d40s http://127.0.0.1:3000/Running 40s test @ http://127.0.0.1:3000/ 2 threads and 100 connections Latency avg 0.90ms · p99 1.60ms Req/Sec 51.2k per thread 4,096,360 requests in 40.00sRequests/sec: 102,409.23
Same code. Every runtime.
Your business logic doesn't care where it runs. Move from Node to the edge to Lambda by swapping the entry file. The routes never change.
export const app = await createApp(); app.get('/users/:id') .params(z.object({ id: z.string().uuid() })) .handler((req) => ({ id: req.params.id })); app.post('/users') .body(z.object({ name: z.string(), email: z.string().email() })) .handler((req) => ({ id: crypto.randomUUID(), ...req.body }));
app.listen(3000, () => { console.log('Ready on :3000');});
Long-running server on Moro's native engine.
no cold startexport const runtime = 'edge';const app = await createAppEdge();export default app.getHandler();
Streaming responses at the edge, globally.
~15ms coldconst app = await createAppLambda();export const handler = app.getHandler();
API Gateway plus Lambda, zero-config handler.
~95ms coldconst app = await createAppWorker();export default { fetch: app.getHandler() };
300+ POPs, V8 isolates, KV and Durable Objects.
~8ms coldIt grows the way projects actually grow
Everything below ships with the framework and loads the first time you call it. Until then it costs nothing, in bytes or in boot time.
Modules with versions
Group routes into modules and versioning happens for you.
version: '1.0.0', ... })
// mounted at /api/v1.0.0/users
Jobs, same file
Cron, macros, or intervals. No second service to deploy.
() => db.sessions.sweep())
One line of npm ls
The whole third-party audit surface of a new app:
Docs write themselves
Routes self-describe, so OpenAPI docs exist the moment the server boots.
One config file
moro.config.ts drives every environment. Env vars override it cleanly, and the CLI validates it.
└── moro.config.ts ← the whole story
One canonical shape
Routes, modules and jobs each have exactly one way to be written, so reviews get shorter, new people get productive sooner, and coding assistants stop inventing APIs that were never there.
And the rest of a backend
Each of these is a first call away. None of them load before that.
Or try it right here
The playground runs a full Moro app in your browser tab. Nothing to install, no account to make, and closing the tab is all the cleanup there is.
Open the playground1app.get('/', () => ({ hello: 'moro' }));2 3// → { "hello": "moro" } · 2ms
Give it sixty seconds
If it turns out not to be what you wanted, delete the folder. You're out one minute.