-
Notifications
You must be signed in to change notification settings - Fork 0
Core Concepts
NextRush is a small functional core — one Application object that composes middleware and routes into a request handler — with runtimes built on top. There is no plugin framework bolted on the side; the whole request path is one composed pipeline of async middleware. Understand those three ideas and the rest of the framework is detail.
createApp() from the nextrush meta-package gives you a single Application. It is the one object where a server declares everything it depends on before accepting a request: middleware, routes, and long-lived services (called Extensions, not plugins).
import { createApp, createRouter, listen } from 'nextrush';
const app = createApp();Three methods build the pipeline, one freezes it:
-
use(...middleware)pushes middleware onto a stack, in call order. -
route(path, router)mounts a router at a path prefix. A mounted router is middleware from the app's point of view. -
get/post/put/patch/delete/head/alldelegate straight to the app-owned router (app.router). -
ready()boots every registered Extension'ssetup()once, mounts the app-owned router last, then freezes configuration —use(),route(),extend()all throw afterward.listen()callsready()for you.
createApp() from nextrush wires a default router in, so app.get() works out of the box. The lower-level @nextrush/core createApp() has no router unless you pass one explicitly (createApp({ router: createRouter() })).
Every request gets a single ctx object, handed from the adapter through every middleware to your handler and back — never copied, never replaced. Handlers write through ctx; they never return a Response object. The adapter builds ctx from the platform's real request at the front and serializes it back at the end.
ctx has three faces:
| Face | Members | Notes |
|---|---|---|
| Input |
ctx.method, ctx.path, ctx.query, ctx.headers, ctx.ip, ctx.get(name), ctx.params, ctx.body
|
params is populated by the router during matching. body is undefined until a body-parser middleware fills it in. |
| Shared |
ctx.state, ctx.next()
|
state is a plain object the whole pipeline reads and writes; next() passes control onward. |
| Output |
ctx.status, ctx.json(), ctx.send(), ctx.html(), ctx.redirect(), ctx.set()
|
Set status, then send exactly once. |
import { createApp, createRouter, listen } from 'nextrush';
const app = createApp();
const router = createRouter();
router.get('/users/:id', (ctx) => {
const id = ctx.params.id; // input: captured by the router
const fields = ctx.query.fields; // input: parsed query string
ctx.json({ id, fields }); // output: JSON response
});
app.route('/', router);
await listen(app, 8080);Everything the client sends arrives on ctx unvalidated. ctx guarantees a consistent shape across runtimes, never safe values — validating params/query/body is the handler's job. ctx.raw exposes the platform's native object and breaks cross-runtime portability; avoid it.
A handler (or middleware) is an async function of ctx that sends a response through ctx and returns undefined. No (req, res) pair, no return new Response(...):
router.get('/health', (ctx) => ctx.json({ ok: true }));Middleware has two equivalent shapes — ctx.next() on the context, or next as the second argument:
import type { Middleware } from 'nextrush';
const a: Middleware = async (ctx) => { await ctx.next(); };
const b: Middleware = async (ctx, next) => { await next(); };Both drive the same dispatch. See Middleware for the onion model and why order matters.
Cross-cutting work (auth, logging, timing, body parsing) is registered once, in front of the handlers, instead of copy-pasted into every one. A middleware does its slice of work, calls await ctx.next(), and — because control comes back — can act after the response is produced too. See Middleware.
NextRush exposes two paradigms over the same core:
-
Functional core (
nextrush) —createApp,createRouter,compose,listen, HTTP errors, and typedMiddleware/Context. This is what this page uses. -
Class runtime (
nextrush/class) —Controller,Get,@Service(), DI, guards. Built on the sameApplication; see Controllers-and-Decorators and Dependency-Injection.
A class-based app composes features into an AppModule and registers them with one call:
import { createApp } from 'nextrush';
import { Module, registerModule } from 'nextrush/class';
@Module({ controllers: [UserController] }) // a feature owns controllers + providers
class UsersModule {}
@Module({ imports: [UsersModule] }) // the root — compose every feature
class AppModule {}
const app = createApp();
await registerModule(app, AppModule);Same application code runs unchanged on Node, Bun, Deno, and edge — only the adapter differs.
import { createApp, createRouter, listen } from 'nextrush';
const app = createApp();
const router = createRouter();
router.get('/', (ctx) => ctx.json({ status: 'ok' }));
app.route('/', router);
await listen(app, 8080);The server answers GET / on port 8080. From here: Routing, Middleware, and Request-Lifecycle walk the pieces in depth.
NextRush · MIT License · Docs · Issues