Design Principles
The philosophy behind NextRush's architecture — minimal core, explicit composition, and zero runtime dependencies.
NextRush's design decisions trace back to three commitments stated in the project's own README and enforced as hard rules in its engineering standards: a minimal core, explicit behavior over hidden magic, and zero external runtime dependencies. This page explains what each commitment means in practice and where the codebase holds the line — or doesn't.
Minimal core
@nextrush/core targets 1,500 lines of code as a hard ceiling (see Package
Hierarchy). The README states the whole framework goal as
"Under 3,000 lines of code" for the core. This is a deliberate constraint, not an accident: every
capability that isn't Application + middleware composition lives in its own package — routing
is @nextrush/router, dependency injection is @nextrush/di, HTTP errors are @nextrush/errors.
The practical effect is that createApp() returns something you can read end-to-end in one
sitting. There is no framework-wide singleton, no implicit global router, no hidden plugin
registry inside core — those are composed in by the packages you choose to install.
Explicit over implicit
Two places this shows up directly in the API surface:
- Middleware controls its own flow. NextRush middleware calls
ctx.next()(or accepts(ctx, next)) explicitly — there is no automatic chaining based on return values or thrown sentinels.compose()in@nextrush/coreis an index-based dispatcher with no hidden control flow (see Middleware Flow); nothing about the pipeline is inferred from the middleware's return type. - Composition over convention-based discovery, by default.
createRouter()andapp.route()are explicit calls — a router isn't auto-mounted by file location unless you opt intoregisterControllers's filesystem discovery mode, which is itself an explicit call, not something that runs implicitly oncreateApp().
The dependency injection layer follows the same rule at a smaller scale: @Service() defaults to
singleton scope, but every other scope (transient, request) is an explicit option, not an
inferred behavior based on how a class is used (see DI Internals).
Zero runtime dependencies
Per the repository's global-rules.instructions.md, core, router, errors, types, adapters, and
middleware packages carry no external runtime dependencies except three named, audited
exceptions: reflect-metadata (decorator metadata for DI), tsyringe (the DI container itself,
scoped to @nextrush/di only), and @clack/prompts (interactive CLI prompts, scoped to the
create-nextrush scaffolder only). The generated dependency graph
confirms this directly: scanning every packages/*/package.json shows no other external runtime
dependency anywhere in the 36-package tree except one additional case — @nextrush/logger
depends on the separately published @nextrush/log package (^0.2.1), which is outside this
rule's three named exceptions and is flagged here rather than smoothed over.
Why this constraint exists
A zero-dependency core means the framework's attack surface, cold-start time, and version-drift risk are bounded by NextRush's own code, not by a transitive dependency tree the maintainers don't control. The tradeoff is that anything a dependency would normally provide — a JSON schema validator, a template engine — has to be either vendored (if under ~50 lines) or shipped as an opt-in package the application explicitly installs.
Composition over inheritance
The class runtime (@nextrush/class) reads like decorators and DI, which can look
inheritance-heavy from the outside. In practice, the object model is composition throughout:
a @Controller doesn't extend a base controller class, a @Service doesn't extend a base
service class, and @Module composes other modules via imports rather than a subclassing
hierarchy. registerControllers and registerModule walk a graph of plain classes wired
together by DI, not an inheritance tree.
Where the constraints were catching up to the code (now closed)
Design principles are only useful if they're checked against what actually ships. Two gaps were tracked here rather than glossed over, and both are now fixed:
- The router's internal file was named
radix-tree.ts. The router's own doc comments were already explicit that the data structure is a segment trie, not a compressed radix tree (see Router Internals for the distinction), but the implementation file and itsRadixNodetype predated that terminology correction. Both have since been renamed —radix-tree.ts→segment-trie.ts,RadixNode→TrieNode— closing the naming debt (verified:grep -rni radix packages/router/srcreturns only an intentional negation, "segment trie... not a radix tree"). CONTRIBUTING.mdat the repo root said "Radix tree routing" and listed a package hierarchy (decorators → controllers → adapters) that predated the@nextrush/classconsolidation.CONTRIBUTING.mdhas since been corrected to the current hierarchy and terminology; Contributing documents the verified hierarchy.
Both were small, tracked inconsistencies rather than architectural drift — the design principles above hold at the level that matters (dependency direction, package boundaries, zero-dep audit), and "explicit over implicit" applies to documentation honesty too.