ReferenceRuntime Adapters
@nextrush/adapter-nextjs

Next.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

Package@nextrush/adapter-nextjs
StatusBeta 🚧
Support tierPublic — stable surface once out of beta (ADR-0014)
Included in nextrush?✅ Yes — re-exported as nextrush/nextjs (optional peer, functional installs skip it)
RuntimeUniversal — Node · Bun · Deno · Cloudflare (via OpenNext) — anywhere Next.js itself runs
RequiresNode >=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

PropertyTypeDescription
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

OptionTypeRequiredDefaultDescription
timeoutnumberNo24000 (the edge engine's default)Per-request timeout in ms, raced to a 504
onError(error, ctx) => Response | Promise<Response>Nothe engine's default 500 handlerCustom 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.

Loading diagram...

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

RequirementVersion
NextRush4.x
Next.js>=14.0.0 (App Router only)
Node.js>=22
TypeScript>=5.x

Runtimes

RuntimeSupportedNotes
Node.js >=22Next'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.


Was this helpful?

On this page