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.

$npx @morojs/cli init my-apiRead the docs
v1.8.3 on npm0 dependencies4 runtimesMIT licensed

The whole first minute, nothing skipped.

terminal
  npx @morojs/cli init my-api
 template · api
 validation · zod
 features · auth, docs
 project scaffolded
 dependencies installed

next: cd my-api && npm run dev
Interactive prompts, or pass --fast and skip them all.
The tradeoff

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.

Positioning, not a benchmark. The benchmark is further down the page.
The API

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.

src/routes/users.ts
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.

Performance

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.

Requests per second, plaintextnon-pipelined
node http
68k
uWebSockets
101k
@morojs/engine
102k

July 2026 run. Expect different absolute numbers on your own hardware. Full methodology →

raw output · @morojs/engine 1.1.3
  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
pipelined x10 peaks at 584k req/s (clustered)measured jul 2026 · moro 1.8.0
Write once, deploy anywhere

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.

src/app.ts
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 }));
server.ts
app.listen(3000, () => {  console.log('Ready on :3000');});

Long-running server on Moro's native engine.

no cold start
Past the first minute

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

defineModule({ name: 'users',
  version: '1.0.0', ... })
// mounted at /api/v1.0.0/users

Jobs, same file

Cron, macros, or intervals. No second service to deploy.

app.job('cleanup', '0 2 * * *',
  () => db.sessions.sweep())

One line of npm ls

The whole third-party audit surface of a new app:

$ npm ls @morojs/moro
└── @morojs/[email protected]
0 transitive dependencies

Docs write themselves

Routes self-describe, so OpenAPI docs exist the moment the server boots.

GET /docs → your API, documented

One config file

moro.config.ts drives every environment. Env vars override it cleanly, and the CLI validates it.

my-api/
└── 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.

// there is no second way to do this

And the rest of a backend

Each of these is a first call away. None of them load before that.

AuthGraphQLgRPCQueuesEmailHTTP/2Circuit breakerDIOpenAPISSECaching// lazy, all of it
No install required

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 playground
playground.ts▶ RUN
1app.get('/', () => ({ hello: 'moro' }));2 3// → { "hello": "moro" }  ·  2ms
Start

Give it sixty seconds

If it turns out not to be what you wanted, delete the folder. You're out one minute.

$npx @morojs/cli init my-apiGet started