DEV Community

Shams Ali Shaikh
Shams Ali Shaikh

Posted on

Rhea.js 1.0: a security-first backend framework for Node.js and Express 5

I just released Rhea.js 1.0, an open-source backend framework for Node.js. It sits on top of Express 5 and adds the parts every API ends up needing: a project layout, security defaults, validation, logging, authentication and a CLI that generates code for you.

npx create-rhea my-api
Enter fullscreen mode Exit fullscreen mode

This post explains what it is, why I built it, what you get, and, just as important, what it does not do yet.

The problem

Every Express project I started began the same way. Install Express. Add Helmet. Add CORS. Remember the body size limit. Add a rate limiter. Add a logger and then notice it prints passwords. Write an error handler. Decide on a folder layout. Copy the auth code from the last project.

Two things bothered me about that routine:

  1. Security is opt-in. If you forget a step, nothing warns you. A fresh express() app has no security headers, no limits, and no opinion.
  2. Every team invents its own structure. Nobody can open a new project and know where the validation lives.

Rhea.js is my answer: keep Express as the HTTP engine, and put the architecture and the safe defaults around it.

What Rhea.js is

  • A runtime package, @rheajs/core, with a createApp() function.
  • An auth package, @rheajs/auth (passwords, JWT, refresh tokens, guards).
  • A CLI, @rheajs/cli, and the scaffolder create-rhea.
  • Express 5 underneath. Your routes are normal Express routes. If you know Express, you already know most of Rhea.js.

It is MIT licensed, written in TypeScript, and works with TypeScript or JavaScript, ES Modules or CommonJS.

Get started in one minute

npx create-rhea my-api
cd my-api
npm run dev
Enter fullscreen mode Exit fullscreen mode

The scaffolder asks four things: TypeScript or JavaScript, ES Modules or CommonJS, a database (MongoDB, MySQL or none) and, if you picked a database, whether to add authentication. Then it installs dependencies, runs git init, makes the first commit, and, if you have the GitHub CLI, can create the GitHub repository for you.

Open http://localhost:5000/api/rhea and you have a running API. Add a resource:

npx rhea generate module users
Enter fullscreen mode Exit fullscreen mode

That creates the controller, service, repository, routes, Zod schema, types and an integration test, and registers the module.

What you get by default

Everything below is on from the first request, with no setup:

  • Request IDs on every request and every error response, so you can find a log line from a bug report.
  • Helmet security headers, plus a Permissions-Policy header.
  • CORS denied by default. You list the origins you trust. A wildcard is refused in production.
  • Rate limiting, with an option to share counters through Redis when you run more than one instance.
  • Body size limit, request timeout and JSON-only bodies (a form-encoded body gets a clean 415).
  • Prototype-pollution guard, and an optional guard against MongoDB operator keys such as {"$ne": null} in a login body (on in generated MongoDB projects).
  • One error shape. Throw NotFoundError or ConflictError from anywhere and the client always gets the same JSON. Stack traces never leave the server in production.
  • Structured logs with pino: JSON in production, readable in development, and passwords, tokens and cookies are redacted. In development you also get a short morgan line per request.
  • Validation with Zod for body, query and params, and environment validation that stops startup when your config is wrong.
  • Graceful shutdown and process error handlers.

A route looks like this:

import { Router, sendSuccess, validate, z } from "@rheajs/core";

const createUser = z.object({ name: z.string().min(1).max(100) });

export const users = Router();

users.post("/", validate(createUser), (req, res) =>
  sendSuccess(res, { name: req.body.name }, "Created", 201),
);
Enter fullscreen mode Exit fullscreen mode

Invalid input returns 422 with a list of exactly which field failed, and the keys Zod strips never reach your handler.

Authentication that is not a tutorial snippet

Pick a database and say yes to authentication, and the generated project gets a complete flow:

  • POST /api/auth/register, login, refresh, logout, logout-all and GET /api/auth/me.
  • Short-lived access token (a JWT, 15 minutes) and a rotating refresh token. The refresh token is random, stored only as a SHA-256 hash, and replaced every time you use it.
  • Reuse detection. If an old refresh token is used again (a sign of theft), the whole session family is revoked. A short grace window handles honest double requests such as a flaky network.
  • Cookie mode for browser apps. Set AUTH_REFRESH_COOKIE=true and the refresh token lives in an HttpOnly, SameSite=Strict cookie, with an Origin check against CSRF. No token in localStorage.
  • Account lockout after five wrong passwords, and a common-password blocklist.
  • scrypt password hashing with OWASP-level parameters.

Protecting your own route is one line:

import { authenticate, requireRole } from "@rheajs/auth";

router.delete("/:id", authenticate(jwt), requireRole("admin"), handler);
Enter fullscreen mode Exit fullscreen mode

I tested this flow against real MongoDB 7 and MySQL 8.4 over HTTP, including ten parallel refreshes with the same token (exactly one wins), and with a Next.js todo app in a real browser.

The CLI

Command What it does
rhea create scaffold a project
rhea dev run with auto restart and graceful shutdown
rhea generate module users controller, service, repository, routes, schema, test
rhea doctor check your setup
rhea security static scan for common config mistakes (--audit adds npm audit)
rhea docker write a multi-stage, non-root Dockerfile
rhea build / rhea start production build and run

Why not NestJS, Fastify or plain Express?

Honest answer: they are all good, and the right choice depends on you.

  • Plain Express is the fastest to start and the slowest to make safe. Rhea.js is Express plus the checklist.
  • NestJS is a large, decorator-based framework with dependency injection. Rhea.js is much smaller and has no decorators. It is plain functions and modules.
  • Fastify is faster and has its own ecosystem. If raw throughput is your top need, use Fastify.

Choose Rhea.js if you want Express and a safe starting point, with a layout your whole team can predict.

The honest part

  • Performance. On a trivial route Rhea.js reaches about 60% of the throughput of bare Express on my machine, because it does real work on every request (IDs, logging, headers, limits, checks). The numbers and the method are in BENCHMARKS.md. If you need raw speed and none of the checks, use bare Express.
  • Security review. Rhea.js is used in production by me. It has not had an independent security review. I wrote a self-audit that lists what was tested, what was fixed, and what is still open. If you can review it, I would love that.
  • Not included yet: server-side sessions, OAuth/OIDC, MFA, password reset, and an ORM. The rate limit is per process unless you pass a shared store.
  • Stability. From 1.0 it follows semantic versioning, and the docs say exactly what the public API is.

Try it and tell me what breaks

npx create-rhea my-api
Enter fullscreen mode Exit fullscreen mode

If you try it, I would like to hear what was confusing or broken. Bug reports, questions and pull requests are all welcome. If you find a vulnerability, please follow SECURITY.md and report it privately.

Thanks for reading.

Top comments (0)