Docs
CLI
Migrations
Compare
Benchmarks
Examples

© 2024 MoroJs

One framework for your whole backend

Everything you need.Only when you need it.

Most Node frameworks give you routing and leave the rest up to you. Moro comes with auth, validation, WebSockets, gRPC, and intelligent routing already built in, so you use what your app needs and nothing else. The same code runs on Node, Edge, Lambda, and Workers.

200k+
req/sec
with uWebSockets.js
0
config
Files required
4
runtimes
One codebase
100%
typed
End-to-end safety
zsh — morojs
~$
The Solution

One framework.Everything included.

Routing, validation, auth, real-time, multi-runtime. The pieces you'd normally wire up yourself are already in the framework.

Schema-first routing in 6 lines

Validate, type, document, and handle a route in one chain. No middleware files, no DTOs, no decorators.

  • Type inference
    req.body and req.query are fully typed from your schema.
  • Auto-validation
    400 with a structured error before your handler ever runs.
  • Universal validators
    Swap Zod for Joi, Yup, or Class Validator without rewriting.
app.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 fully typed and already validated
    return { id: crypto.randomUUID(), ...req.body }
  })

app.listen(3000)
The signature feature

Middleware thatorders itself.

Add cors, auth, rate-limit, and validation in whatever order you like. MoroJS works out the dependencies and runs them in the right order, so a refactor can't quietly break your pipeline.

app.ts — as you wrote itSource
app.post('/users')
// any order works
.cache({ ttl: 60 })
.body(CreateUserSchema)
.rateLimit({ requests: 100, window: 60_000 })
.auth({ roles: ['user'] })
.handler(createUser)
MoroJS execution order
1cache
2body(schema)
3rateLimit
4auth
5handler
No race conditions
Auth always runs before authorization. Validation always before handler.
Order-agnostic
Refactor freely. Your middleware won't silently break.
Predictable performance
Cheap middleware (cors, rate-limit) runs first. Heavy work runs last.
Write once. Deploy anywhere.

Same code.Every runtime.

Your business logic doesn't care where it runs. Move from Node to Edge to Lambda by swapping the entry file. The routes never change.

app.ts
Identical in every runtime
// app.ts — your application code, identical in every runtime
import { createApp, z } from '@morojs/moro'

export const app = await createApp()

app.get('/users/:id')
  .params(z.object({ id: z.string().uuid() }))
  .handler((req) => {
    return { id: req.params.id, name: `User ${req.params.id}` }
  })

app.post('/users')
  .body(z.object({ name: z.string(), email: z.string().email() }))
  .handler((req) => ({ id: crypto.randomUUID(), ...req.body }))
server.tsNode.js
// server.ts
import { app } from './app'

app.listen(3000, () => {
  console.log('Ready on :3000 — node runtime')
})

Long-running server, full uWebSockets.js perf available.

More than just an HTTP server

Versioned modules.Built-in scheduler.

Drop in a folder and get a versioned API surface. Cron jobs, mail, and worker threads all run from the same app instance, with no extra services to stand up.

modules/users/index.ts
Module system
// modules/users/index.ts — versioned, isolated, testable
import { defineModule } from '@morojs/moro'
import * as actions from './actions'
import * as schemas from './schemas'

export const usersModule = defineModule({
  name: 'users',
  version: '1.0.0',
  routes: [
    {
      method: 'POST',
      path: '/users',
      handler: actions.createUser,
      validation: { body: schemas.CreateUserSchema },
      rateLimit: { requests: 10, window: 60_000 },
      description: 'Create a new user',
    },
  ],
})

// Mounted at /api/v1.0.0/users — versioning is automatic.
jobs.ts
Job scheduler
// background work, in the same app
import { app, createApp } from '@morojs/moro'

// Cron expression
app.job('cleanup', '0 2 * * *', async () => {
  await db.sessions.deleteExpired()
})

// Macro
app.job('daily-report', '@daily', async (ctx) => {
  await mail.send({ subject: 'Daily report', html: report() })
})

// Interval string — '5m', '1h', '30s'
app.job('health-check', '5m', async (ctx) => {
  ctx.logger.debug('alive', ctx.executionId)
})
Versioned modules

Auto-mounted at /api/v{version}/{module}.

Cron + intervals

Cron expressions, @daily macros, "5m" strings.

Mail adapters

SES, SendGrid, Resend, Nodemailer — same API.

DB adapters

PostgreSQL, MySQL, MongoDB, SQLite, Redis, Drizzle.

Batteries included

Everything you need.Built in. Type-safe.

22 first-class features, all built in. No plugin ecosystem to learn, no "what do I use for X" rabbit holes. It's already in the box.

See how MoroJS compares to Express, Fastify, NestJS, and Hono
Try it now

No install.Just run it.

A real MoroJS endpoint, executing right here. Change the input, hit run, see the response.

import { createApp, z } from '@morojs/moro'

const app = await createApp()

app.get('/hello')
  .query(z.object({
    name: z.string().optional().default('World'),
  }))
  .handler((req) => ({
    message: `Hello, ${req.query.name}!`,
    timestamp: new Date().toISOString(),
  }))

app.listen(3000)
GET/hello?name=
Response
Press Run to execute.

Build it inthe next 60 seconds.

One command. A typed, validated, production-ready API. No framework decisions left to make.

MIT licensed · No telemetry · Open governance