Migrate

Breaking Changes

Real v4 API breaks: retired shims, backward-compat alias removals, and renamed internals — every item verified against source and commit history.

Every item below is a real, shipped change — verified against packages/*/src, CHANGELOG.md, and the commit that made it, not inferred from another framework's conventions. If you're upgrading from an older NextRush install, read this before Upgrade guide.

Not the same as the docs-site reorg

This page covers changes to the framework's API and packages. If you're looking for where a documentation page moved, see v3 → v4 docs map instead.

@nextrush/decorators and @nextrush/controllers were removed

Both packages were pure re-export shims over @nextrush/class — confirmed by reading each package's src/index.ts before removal, neither contained logic of its own. They no longer exist in this repository and no longer publish new versions.

- import { Controller, Get, UseGuard } from '@nextrush/decorators';
- import { registerControllers } from '@nextrush/controllers';
+ import { Controller, Get, UseGuard, registerControllers } from 'nextrush/class';

Every symbol either package re-exported still exists under nextrush/class — see Deprecations for the exact symbol-by-symbol map, or run the automated codemod:

nextrush codemod consolidate-imports "src/**/*.ts"

Their last-published npm versions (3.1.0) keep working as-is in an existing lockfile; this is a removal from the repository going forward, not a retroactive break.

A batch of dead backward-compatibility aliases was removed

Confirmed superseded with zero remaining internal use before removal. If your code references any of these, switch to the replacement:

PackageRemovedUse instead
@nextrush/adapter-{bun,deno,edge}{Bun,Deno,Edge}BodySource type/value aliases, create{Bun,Deno,Edge}BodySourceWebBodySource / createWebBodySource from @nextrush/runtime
@nextrush/corecreateHttpError aliascreateError
@nextrush/errorsErrorContext, ErrorMiddlewareContext / Middleware from @nextrush/types
@nextrush/errorscatchAsync()Remove the call — it was a no-op wrapper
@nextrush/body-parserNode-stream fallback path: BodyParserContext.raw, RequestStream, BodyParserMiddlewareMiddleware from @nextrush/types; requires ctx.bodySource, which every current adapter provides
@nextrush/helmetframeguard(), the frameguard option, XFrameOptionsValueThe CSP frame-ancestors directive, honored by every modern browser
@nextrush/corsCorsMiddleware aliasMiddleware from @nextrush/types

Router internals renamed: radix tree → segment trie

@nextrush/router's internal data structure and its public-facing name both changed from "radix tree" to "segment trie" — RadixNode is now TrieNode, and radix-tree.ts is now segment-trie.ts. RadixNode was never part of the public runtime export surface (confirmed against the package's own public-surface.test.ts), so this is a naming/documentation correction, not an API removal. If you see "radix tree" anywhere in older NextRush documentation or steering files, it's describing a previous implementation and is stale — the router has always been, and remains, a segment trie: O(k) lookup per path segment, independent of route count.

reference/ URL structure flattened

Reference documentation URLs lost a redundant middle segment:

- /docs/reference/core/router
+ /docs/reference/router
- /docs/reference/middleware/cors
+ /docs/reference/cors
- /docs/reference/plugins/events
+ /docs/reference/events
- /docs/reference/adapters/node
+ /docs/reference/platforms/node

Old links redirect automatically — see v3 → v4 docs map for the full table. This is a documentation-site URL change; it does not affect the packages' JavaScript/TypeScript import paths.

internals/ renamed to architecture/

The docs section documenting NextRush's own internal design moved from /docs/internals to /docs/architecture (a documentation URL rename, not a package or import change). Its contributing.mdx page was merged into /docs/community/contributing.

"Plugin" renamed to "Extension"

The concept previously called a "Plugin" — a long-lived, app-scoped service wired with app.extend() and booted with app.ready() — is now called an Extension throughout the documentation and steering. This is a terminology and documentation change: app.extend() and app.ready() are unchanged; only the noun used to describe what you're registering changed from "Plugin" to "Extension." @nextrush/class's ClassRef and internal helpers (deepFreeze, bootstrapPipeline) were also sealed off the package's public surface around the same time (see ADR-0005) — if you were importing any of those from @nextrush/class, they were never part of the supported contract and were never re-exported by the removed shims either.

Not a breaking change: mixed-version monorepo

Packages that shipped with the original v3 release version together at 3.1.0; packages added afterward (@nextrush/cookies, @nextrush/csrf, @nextrush/form-data, and others) start their own semver line at 1.0.0 and version independently. A 1.0.0 package is not less mature than a 3.1.0 one — it started its own line later. See the Compatibility Matrix for the current, real version of every package.

The harden-security-boundaries change closed eleven security findings across the router, proxy trust, CSRF, cookies, and response/CORS boundaries. Six of its fixes are breaking API changes — each is verified against the current source in packages/*/src, not inferred:

ctx.path is now the canonical, case-folded path; ctx.originalPath is new

ctx.path previously published the raw request target. It now publishes the router's canonical path (case-folded, slash-collapsed, dot-segments rejected before your middleware ever runs) — the same value the router matched against, so a path-prefix check on ctx.path can no longer be bypassed by a differently-cased or double-slashed request (the SEC-02 authorization bypass this change closes). The raw, un-normalized request target is now available separately as ctx.originalPath.

  app.use(async (ctx, next) => {
-   if (ctx.path.startsWith('/admin')) { /* case-sensitive, bypassable */ }
+   if (ctx.path.startsWith('/admin')) { /* now case-folded and dot-segment-safe */ }
    await next();
  });

+ // If you need the exact bytes the client sent (e.g. for logging):
+ logger.info(`raw target: ${ctx.originalPath}`);

A request whose target contains a dot segment (/api/../admin) now receives 400 before any middleware runs, rather than being resolved and dispatched.

proxy: true no longer exists — proxy is false | number | string[]

createApp({ proxy: true }) trusted every X-Forwarded-For entry unconditionally — the SEC-01 finding that let a client forge its own IP address for rate-limit and allow/deny-list purposes. proxy is now false (trust nothing — the default), a number (trust exactly that many reverse-proxy hops), or a string[] of trusted peer CIDRs/IPs. Both proxy: true and proxy: 0 throw at construction, naming the replacement.

- createApp({ proxy: true })
+ createApp({ proxy: 1 })              // one reverse proxy (e.g. a single nginx/ALB hop)
+ createApp({ proxy: ['10.0.0.0/8'] }) // only these peers are trusted

Edge/Workers adapters additionally refuse a peer-CIDR list at boot when the platform has no direct peer address to compare against — see Deprecations for the per-adapter detail.

Signed cookies use a new, context-bound signature format

@nextrush/cookies' signed cookies previously signed the value alone, so a signature valid for one cookie name verified successfully when replayed under a different name (SEC-07). Signatures now cover a length-prefixed name|value|issuedAt tuple. A cookie signed under the previous value-only format fails verification by default. Set acceptLegacySignatures: true to accept the old format during a rotation window — this compatibility path is deprecated from the moment it ships and is intended to be temporary, not a permanent option.

  signedCookies({
    secret,
+   acceptLegacySignatures: true, // only while migrating existing signed cookies
  })

Remove acceptLegacySignatures once every previously-issued signed cookie has expired or been reissued.

csrf() now requires an explicit session-binding decision and an origin allowlist

Two omissions that previously fell back to a weaker default now throw at construction instead:

  csrf({
    secret,
+   getSessionIdentifier: (ctx) => ctx.state.sessionId,
+   // — or, to explicitly opt into the weaker unbound double-submit mode:
+   // sessionBinding: 'none',
+   allowedOrigins: ['https://app.example.com'],
  })
  • Session binding is no longer implicit. csrf() throws unless you provide either getSessionIdentifier or an explicit sessionBinding: 'none' acknowledgement (SEC-05) — a token minted for one session can no longer be silently accepted under a different one.
  • originCheck defaults to true and requires allowedOrigins. Origin validation now compares the Origin header against your configured allowlist only — never against the attacker-controlled Host header (SEC-04). csrf() throws at construction if originCheck is left at its new default with no allowedOrigins configured, since there would be nothing safe left to compare against.

The default token extractor also no longer reads ?_csrf= from the query string (tokens no longer reach access logs or Referer headers by default); supply a custom getTokenFromRequest if you relied on the query-string fallback.

CORS no longer echoes Access-Control-Request-Headers — it intersects a fixed allowlist

A preflight requesting any header previously received that exact header back in Access-Control-Allow-Headers (SEC-10) — an attacker-controlled request header choosing what the response allows. @nextrush/cors now ships a conservative default allowlist (Content-Type, Accept, X-Requested-With) and responds with the intersection of the requested headers and that allowlist; Authorization is included only when credentials: true. A preflight requesting a header outside the allowlist no longer sees it echoed back.

  app.use(cors({
    origin: 'https://app.example.com',
+   allowedHeaders: ['Content-Type', 'Accept', 'X-Requested-With', 'X-My-Custom-Header'],
  }))

If your client sends a custom header not in the new default set, add it explicitly via allowedHeaders — the previous echo-back behavior is not configurable back on, by design.

Not flipped in this change: caseSensitive

@nextrush/router's caseSensitive option default was evaluated for a flip (from false to true) as part of this same hardening pass, since case-folding is exactly what makes ctx.path canonicalization possible. It was deferred, not shipped — flipping a router-wide default is a separate, major-version-gated decision from the P0–P2 fixes above, and is tracked independently (RFC-029 §15) rather than bundled into this change. If you see a reference to a caseSensitive default flip in an RFC or design document for this change, treat the deferral above — not the RFC's original proposal — as the shipped behavior.

Next steps

Was this helpful?

On this page