@nextrush/adapter-nextjsNext.js
Mount a NextRush application inside a Next.js App Router route handler.
Bridge a NextRush Application into the seven exports a Next.js App Router route.ts needs —
one function, zero request rewriting.
Source & internals
README · ARCHITECTURE · see also Adapter Contract
| Package | @nextrush/adapter-nextjs |
| Status | Beta 🚧 |
| Support tier | Public — stable surface once out of beta (ADR-0014) |
Included in nextrush? | ✅ Yes — re-exported as nextrush/nextjs (optional peer, functional installs skip it) |
| Runtime | Universal — Node · Bun · Deno · Cloudflare (via OpenNext) — anywhere Next.js itself runs |
| Requires | Node >=22 · Next.js >=14.0.0 (App Router only) · TypeScript >=5.x |
App Router only
The Pages Router is a permanent non-goal — it hands (req, res), not a Request, and
supporting it would pull Node-specific code into what is otherwise a fully Web-standard
package. Migrate the route to app/api/[[...route]]/route.ts. See
rejected alternatives for the full reasoning.
Installation
$ pnpm add @nextrush/adapter-nextjs
Already using nextrush? Import from nextrush/nextjs instead — same package, re-exported as an
optional peer. A functional-only pnpm add nextrush never resolves it or next.
Quick reference
// src/server/app.ts
import { createApp, createRouter } from 'nextrush';
const app = createApp();
const api = createRouter();
api.get('/hello', (ctx) => ctx.json({ message: 'Hello Next.js!' }));
app.route('/api', api);
export { app };// app/api/[[...route]]/route.ts
import { app } from '@/server/app';
import { handle } from 'nextrush/nextjs';
export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } = handle(app);For the full walkthrough — project structure, class-based apps, the Next 14 caching caveat — see the Next.js tutorial.
API reference
handle(app, options?)
Mounts an Application and returns all seven Next.js App Router HTTP-method exports.
import { handle } from 'nextrush/nextjs';
export const { GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS } = handle(app);Also accepts a factory — sync or async — for apps that need to build asynchronously (class-based
apps registering modules, for example). AppModule is an ordinary @Module class — see the
modules concept for how @Module/@Controller/@Service compose, and
the Next.js tutorial for the full, runnable version of this
example:
Requires enabling legacy decorators in tsconfig.json
@nextrush/class needs experimentalDecorators + emitDecoratorMetadata — Next.js's SWC
compiler supports both (reading them from tsconfig.json), but a fresh Next.js project doesn't
enable them by default. This class-based path has not yet been run through a real next build
in this project's own verification (unlike the plain functional path, verified against Next
14/15/16) — see the tutorial's full caveat before relying on it.
import { createApp } from 'nextrush';
import { registerModule } from 'nextrush/class';
import { AppModule } from '@/server/app.module';
export const { GET, POST } = handle(async () => {
const app = createApp();
await registerModule(app, AppModule, { prefix: '/api' });
return app;
});NextHandlerOptions
| Property | Type | Description |
|---|---|---|
timeout? | number= 24000 (the edge engine default) | Per-request timeout in milliseconds, raced against the handler. Returns a 504 Gateway Timeout on expiry. Pass-through to the underlying @nextrush/adapter-edge engine. |
onError? | (error: Error, ctx: EdgeContext) => Response | Promise<Response> | Custom error → Response mapping. Pass-through to the underlying engine's default 500 handler. |
type AppSource
An Application, or a (possibly async) factory producing one:
type AppSource = Application | (() => Application | Promise<Application>);type NextRouteHandlers
The shape handle() returns — exactly the seven methods Next.js route handlers support:
type NextRouteHandlers = {
GET: RouteHandlerFn;
POST: RouteHandlerFn;
PUT: RouteHandlerFn;
PATCH: RouteHandlerFn;
DELETE: RouteHandlerFn;
HEAD: RouteHandlerFn;
OPTIONS: RouteHandlerFn;
};type NextRouteContext
The structural shape of Next's second handler argument — matches Next's own generated
RouteContext type (params as a Promise, per Next 15+'s async-params convention):
type NextRouteContext = {
params: Promise<Record<string, string | string[] | undefined>>;
};Options
| Option | Type | Required | Default | Description |
|---|---|---|---|---|
timeout | number | No | 24000 (the edge engine's default) | Per-request timeout in ms, raced to a 504 |
onError | (error, ctx) => Response | Promise<Response> | No | the engine's default 500 handler | Custom error → Response mapping |
Both options are pass-throughs to @nextrush/adapter-edge's createFetchHandler — this package
adds no timeout or error-handling logic of its own (see Mental model below).
Mental model
handle() resolves the Application once (memoized for the life of the module instance), builds
one createFetchHandler engine over it, and returns seven functions that all dispatch through
that same engine. It adds no execution model, no context type, and no error handling of its own —
every one of those is inherited from @nextrush/adapter-edge unmodified.
Invariant: the request forwarded to the engine is never modified — no Request
reconstruction, ever. Mount prefixes are declared by the application (app.route()), never
inferred or configured by this package.
ctx.waitUntil() and after()
Next.js supplies no execution context of its own the way Cloudflare or Vercel Edge do — without
this adapter, ctx.waitUntil() silently no-ops under a hand-rolled bridge. handle() resolves
Next's after() export as a capability probe (once, process-lifetime) and wires
ctx.waitUntil() to it automatically when available:
app.use(async (ctx) => {
ctx.waitUntil(
fetch('https://analytics.example.com/track', {
method: 'POST',
body: JSON.stringify({ path: ctx.path }),
})
);
ctx.json({ ok: true });
});A synchronous throw from after() itself is swallowed
EdgeContext.waitUntil is documented to never throw from the caller's perspective, and this
package preserves that contract even if the underlying after() implementation violates it
(e.g. Next's own "called outside a request scope" error). This is intentional, not a bug.
Compatibility
Requirements
| Requirement | Version |
|---|---|
| NextRush | 4.x |
| Next.js | >=14.0.0 (App Router only) |
| Node.js | >=22 |
| TypeScript | >=5.x |
Runtimes
| Runtime | Supported | Notes |
|---|---|---|
Node.js >=22 | ✅ | Next's default runtime |
| Bun / Deno / Cloudflare (OpenNext) | ✅ | The package imports no runtime-specific API — verified by packages/adapters/conformance's nextjs driver |
Integration
- Peer dependencies:
next >=14.0.0(optional, resolved lazily) - Depends on:
@nextrush/adapter-edge— reuses its fetch engine, unmodified - Works with: any NextRush middleware/class-runtime package — the mounted app is an ordinary
Application - Incompatible with: the Pages Router — see rejected alternatives
Rejected alternatives
Recorded here because each was seriously considered during design — not obvious in hindsight:
Stripping the mount prefix by rewriting the request
Rejected on correctness grounds. @nextrush/runtime's context base computes url/path/query
eagerly from request.url in its constructor, so a rewritten request makes the mount prefix
permanently unrecoverable inside the handler — breaking relative redirects and any generated
OpenAPI paths. The earlier design mitigated this with an x-forwarded-prefix header and a
getMountPath() helper; both were deleted once prepend was adopted, because there was nothing
left to undo.
Supporting the Pages Router
Rejected because it's the only reason this package would need node:* or a dependency on
@nextrush/adapter-node — Pages hands (req, res), not a Request. Supporting it would force a
two-subpath split to keep that dependency out of Web-runtime bundles.
The adapter injecting the mount prefix at boot
Rejected because @nextrush/core's application boot mounts the app-owned router once — an app
shared between a Next route file and a standalone listen() call would then route differently
depending on which host booted it first. Mount configuration belongs to the application, never
to the adapter serving it.
Full design history: RFC-024, ADR-0014.
Related
- Next.js tutorial — the full walkthrough: installation, project structure for a real API, class-based apps, and the Next 14 caching caveat.
- Platforms overview — understanding runtime adapters generally.
- @nextrush/adapter-edge — the fetch-handler engine this package builds on, unmodified.
- @nextrush/adapter-serverless — for AWS Lambda/GCF/Azure Functions deployments with no Next.js involved.